This commit is contained in:
zlwu committed 2026-07-17 21:59:47 +08:00
1 parent 8be55c08e9
commit a903fcf6d4
75 files changed
+9547 -124

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
/** 报销状态 / 金额展示 */
const STATUS = {
draft: { text: '待提交', tag: 'muted' },
pending: { text: '审批中', tag: 'warning' },
approved: { text: '已通过', tag: 'success' },
rejected: { text: '已驳回', tag: 'danger' },
issued: { text: '已发放', tag: 'success' },
paid: { text: '已付款', tag: 'success' },
processed: { text: '已处理', tag: 'success' },
offset: { text: '已冲抵', tag: 'info' }
}
export function bxStatusLabel(status) {
if (status == null || status === '') return { text: '—', tag: 'muted' }
return STATUS[String(status)] || { text: String(status), tag: 'muted' }
}
export function formatMoney(val) {
if (val == null || val === '') return '—'
const n = Number(val)
if (Number.isNaN(n)) return String(val)
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
export function dash(val) {
if (val == null || val === '') return '—'
return String(val)
}
export function todayStr() {
const d = new Date()
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const day = `${d.getDate()}`.padStart(2, '0')
return `${d.getFullYear()}-${m}-${day}`
}
+89
View File
@@ -0,0 +1,89 @@
/**
* 报销附件:拍照/相册 → /attachments/upload(multipart,已在加密排除路径)
* 返回附件对象列表(含 id),提交后需 bindAttachments
*/
import { API_BASE, TOKEN_KEY } from '@/config/api'
function parseBody(raw) {
try {
return JSON.parse(typeof raw === 'string' ? raw : String(raw || ''))
} catch {
return {}
}
}
function getToken() {
const raw = uni.getStorageSync(TOKEN_KEY)
return typeof raw === 'string' ? raw.trim() : raw ? String(raw).trim() : ''
}
/**
* @param {{ bizType?: string, bizId?: string|number, maxCount?: number, currentCount?: number }} opts
* @returns {Promise<Array<{ id: string, fileName?: string, localPath?: string }>>}
*/
export async function chooseAndUploadBxAttachments(opts = {}) {
const bizType = opts.bizType || 'reimburse_apply'
const bizId = opts.bizId != null ? String(opts.bizId) : '0'
const maxCount = opts.maxCount != null ? opts.maxCount : 6
const currentCount = opts.currentCount || 0
const remain = maxCount - currentCount
if (remain <= 0) {
uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
return []
}
const choose = await new Promise((resolve) => {
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => resolve(res),
fail: () => resolve(null)
})
})
if (!choose?.tempFilePaths?.length) return []
const token = getToken()
const uploadUrl = `${API_BASE}/attachments/upload?biz_type=${encodeURIComponent(bizType)}&biz_id=${encodeURIComponent(bizId)}`
const uploaded = []
uni.showLoading({ title: '上传中...' })
try {
for (const filePath of choose.tempFilePaths) {
const uploadRes = await new Promise((ok, fail) => {
uni.uploadFile({
url: uploadUrl,
filePath,
name: 'file',
header: {
Authorization: token ? `Bearer ${token}` : ''
},
success: ok,
fail
})
})
if (uploadRes.statusCode === 401) {
uni.showToast({ title: '登录已失效', icon: 'none' })
break
}
const body = parseBody(uploadRes.data)
// bx: code===0
if (body.code !== 0 || !body.data?.id) {
uni.showToast({ title: body.msg || body.message || '上传失败', icon: 'none' })
break
}
uploaded.push({
id: String(body.data.id),
fileName: body.data.fileName || body.data.file_name || '',
localPath: filePath
})
}
} catch (e) {
uni.showToast({
title: e instanceof Error ? e.message : '上传失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
return uploaded
}
+75
View File
@@ -0,0 +1,75 @@
/** CRM 状态/阶段展示(与 Web 字典语义对齐的常用值) */
const MAPS = {
clueStatus: {
new: { text: '新线索', tag: 'info' },
follow: { text: '跟进中', tag: 'warning' },
invalid: { text: '无效', tag: 'danger' },
converted: { text: '已转化', tag: 'success' }
},
opportunityStage: {
initial: { text: '初步接触', tag: 'info' },
demand: { text: '需求确认', tag: 'info' },
proposal: { text: '方案报价', tag: 'warning' },
negotiation: { text: '商务谈判', tag: 'warning' },
won: { text: '赢单', tag: 'success' },
lost: { text: '输单', tag: 'danger' }
},
opportunityStatus: {
following: { text: '跟进中', tag: 'warning' },
won: { text: '赢单', tag: 'success' },
lost: { text: '输单', tag: 'danger' },
invalid: { text: '无效', tag: 'muted' }
},
customerStatus: {
potential: { text: '潜在', tag: 'info' },
intentional: { text: '意向', tag: 'warning' },
dealing: { text: '洽谈', tag: 'warning' },
dealed: { text: '已成交', tag: 'success' },
lost: { text: '流失', tag: 'danger' },
pool: { text: '公海', tag: 'muted' }
},
quoteStatus: {
draft: { text: '草稿', tag: 'muted' },
pending: { text: '待审批', tag: 'warning' },
approved: { text: '已通过', tag: 'success' },
rejected: { text: '已驳回', tag: 'danger' },
invalid: { text: '已作废', tag: 'muted' },
converted: { text: '已转合同', tag: 'success' }
},
contractStatus: {
draft: { text: '草稿', tag: 'muted' },
pending: { text: '待审批', tag: 'warning' },
approved: { text: '已生效', tag: 'success' },
rejected: { text: '已驳回', tag: 'danger' },
executing: { text: '执行中', tag: 'info' },
completed: { text: '已完成', tag: 'success' },
terminated: { text: '已终止', tag: 'danger' },
expired: { text: '已过期', tag: 'warning' }
},
level: {
A: { text: 'A级', tag: 'danger' },
B: { text: 'B级', tag: 'warning' },
C: { text: 'C级', tag: 'info' },
D: { text: 'D级', tag: 'muted' }
}
}
export function labelOf(mapKey, value) {
if (value == null || value === '') return { text: '—', tag: 'muted' }
const hit = MAPS[mapKey]?.[String(value)]
if (hit) return hit
return { text: String(value), tag: 'muted' }
}
export function formatMoney(val) {
if (val == null || val === '') return '—'
const n = Number(val)
if (Number.isNaN(n)) return String(val)
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`
}
export function dash(val) {
if (val == null || val === '') return '—'
return String(val)
}
+65
View File
@@ -0,0 +1,65 @@
/** 地磅状态 / 重量展示 */
export function dash(val) {
if (val == null || val === '') return '—'
return String(val)
}
export function formatWeight(val, unit = 'kg') {
if (val == null || val === '') return '—'
const n = Number(val)
if (Number.isNaN(n)) return String(val)
return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} ${unit}`
}
/** 地磅单阶段:作废 / 已完成 / 一次过磅 / 待过磅 */
export function weighingPhase(row) {
if (!row) return { text: '—', tag: 'muted' }
if (Number(row.isVoided) === 1) return { text: '已作废', tag: 'danger' }
const gross = Number(row.grossWeight) || 0
const tare = Number(row.tareWeight) || 0
if (gross > 0 && tare > 0) return { text: '已完成', tag: 'success' }
if (gross > 0) return { text: '一次过磅', tag: 'warning' }
return { text: '待过磅', tag: 'info' }
}
export function planStatusLabel(status) {
if (!status) return { text: '未开始', tag: 'muted' }
const s = String(status)
if (s.includes('已')) return { text: s, tag: 'success' }
if (s.includes('中')) return { text: s, tag: 'warning' }
if (s.includes('待')) return { text: s, tag: 'info' }
return { text: s, tag: 'muted' }
}
export function inventoryStatusLabel(status) {
if (!status) return { text: '—', tag: 'muted' }
const s = String(status)
if (s === '已完成') return { text: s, tag: 'success' }
if (s.includes('待')) return { text: s, tag: 'warning' }
return { text: s, tag: 'info' }
}
export function todayYmd() {
const d = new Date()
const m = `${d.getMonth() + 1}`.padStart(2, '0')
const day = `${d.getDate()}`.padStart(2, '0')
return `${d.getFullYear()}-${m}-${day}`
}
export function nowYmdHms() {
const d = new Date()
const pad = (n) => `${n}`.padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
/** 客户端关键词过滤(后端 list 多为全量) */
export function filterByKeyword(rows, keyword, fields) {
const kw = String(keyword || '')
.trim()
.toLowerCase()
if (!kw) return rows || []
return (rows || []).filter((row) =>
fields.some((f) => String(row[f] != null ? row[f] : '').toLowerCase().includes(kw))
)
}
+114
View File
@@ -0,0 +1,114 @@
/** 运输计划 / 运费状态展示 */
export function dash(val) {
if (val == null || val === '') return '—'
return String(val)
}
export function formatMoney(val) {
if (val == null || val === '') return '—'
const n = Number(val)
if (Number.isNaN(n)) return String(val)
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
export function formatQty(val, unit = '') {
if (val == null || val === '') return '—'
const n = Number(val)
const s = Number.isNaN(n) ? String(val) : String(n)
return unit ? `${s} ${unit}` : s
}
export function planTypeLabel(planType) {
const map = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
return map[planType] || planType || '—'
}
export function followStatusLabel(status) {
const map = {
draft: { text: '草稿', tag: 'muted' },
processing: { text: '进行中', tag: 'warning' },
completed: { text: '已完成', tag: 'success' },
cancelled: { text: '已作废', tag: 'danger' }
}
return map[status] || { text: status || '—', tag: 'muted' }
}
export function transportStatusLabel(status) {
const map = {
pending: { text: '待安排', tag: 'warning' },
arranged: { text: '已安排', tag: 'info' }
}
return map[status] || { text: status || '—', tag: 'muted' }
}
export function archiveStatusLabel(status) {
const map = {
unarchived: { text: '未归档', tag: 'info' },
archived: { text: '已归档', tag: 'success' }
}
return map[status] || { text: status || '—', tag: 'muted' }
}
/**
* Demo 列表 Tab 语义:待发运 / 在途 / 已完成
* - 待发运:transportStatus=pending 且未作废
* - 在途:已安排且跟进未完成
* - 已完成:followStatus=completed
*/
export function planListPhase(row) {
if (!row) return { text: '—', tag: 'muted', key: 'unknown' }
if (row.followStatus === 'cancelled') return { text: '已作废', tag: 'danger', key: 'cancelled' }
if (row.followStatus === 'completed') return { text: '已完成', tag: 'success', key: 'done' }
if (row.transportStatus === 'pending') return { text: '待发运', tag: 'warning', key: 'pending' }
if (row.transportStatus === 'arranged') return { text: '在途', tag: 'info', key: 'transit' }
return followStatusLabel(row.followStatus)
}
export function settlementStatusLabel(status) {
const map = {
pending: { text: '草稿', tag: 'muted' },
rejected: { text: '已驳回', tag: 'danger' },
approving: { text: '审批中', tag: 'warning' },
approved: { text: '待发放', tag: 'info' },
confirmed: { text: '已发放', tag: 'success' },
cancelled: { text: '已作废', tag: 'danger' }
}
return map[status] || { text: status || '—', tag: 'muted' }
}
export function isEditableDraftSettlement(status) {
return status === 'pending' || status === 'rejected'
}
/** 计划卡片副标题:品名 · 数量 · 路线 */
export function planCardSub(row) {
if (!row) return '—'
const products = Array.isArray(row.products) ? row.products : []
const names = products
.map((p) => p.productName || p.product_name)
.filter(Boolean)
.slice(0, 2)
const namePart = names.length ? names.join('、') : planTypeLabel(row.planType)
const qty = products.reduce((sum, p) => sum + (Number(p.quantity) || Number(p.actualQty) || 0), 0)
const unit = products[0]?.unit || ''
const qtyPart = qty > 0 ? formatQty(qty, unit) : ''
const from = row.pickupAddress || row.supplierName || ''
const to = row.destinationAddress || row.customerName || row.destinationName || ''
const route = from && to ? `${shortAddr(from)} → ${shortAddr(to)}` : dash(row.customerName || row.destinationName)
return [namePart, qtyPart, route].filter(Boolean).join(' · ')
}
function shortAddr(s) {
const t = String(s || '')
return t.length > 10 ? `${t.slice(0, 10)}…` : t
}
export function certReadyLabel(ready) {
return ready ? { text: '证书齐备', tag: 'success' } : { text: '待齐备', tag: 'warning' }
}
export function hasPurchaseCert(product) {
const url = product?.purchaseCertUrl || product?.purchase_cert_url
return !!(url && String(url).trim())
}
+102
View File
@@ -0,0 +1,102 @@
/**
* 小程序拍照凭证上传(OpenSpec 基线)
* chooseImage → uni.uploadFile → 收集 URL → 业务接口再传 imageUrls
*/
import { API_BASE, TOKEN_KEY, UPLOAD_API } from '@/config/api'
function parseUploadBody(raw) {
try {
return JSON.parse(typeof raw === 'string' ? raw : String(raw || ''))
} catch {
return {}
}
}
function extractUrl(body) {
const data = body && body.data
if (!data || typeof data !== 'object') return ''
const url = data.url || data.fileUrl || data.file_url
return url != null ? String(url).trim() : ''
}
function getToken() {
const raw = uni.getStorageSync(TOKEN_KEY)
return typeof raw === 'string' ? raw.trim() : raw ? String(raw).trim() : ''
}
/**
* @param {{ folder?: string, maxCount?: number, currentUrls?: string[], uploadUrl?: string }} opts
* @returns {Promise<string[]>} 本次新上传成功的 URL 列表
*/
export async function chooseAndUploadImages(opts = {}) {
const folder = opts.folder || 'mini-evidence'
const maxCount = opts.maxCount != null ? opts.maxCount : 6
const currentUrls = Array.isArray(opts.currentUrls) ? opts.currentUrls : []
const uploadUrl = opts.uploadUrl || `${API_BASE}${UPLOAD_API.IMAGE}`
const remain = maxCount - currentUrls.length
if (remain <= 0) {
uni.showToast({ title: `最多上传${maxCount}张`, icon: 'none' })
return []
}
const choose = await new Promise((resolve) => {
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => resolve(res),
fail: () => resolve(null)
})
})
if (!choose?.tempFilePaths?.length) return []
const token = getToken()
const urls = []
uni.showLoading({ title: '上传中...' })
try {
for (const filePath of choose.tempFilePaths) {
const uploadRes = await new Promise((ok, fail) => {
uni.uploadFile({
url: uploadUrl,
filePath,
name: 'file',
header: {
Authorization: token ? `Bearer ${token}` : ''
},
formData: { folder },
success: ok,
fail
})
})
if (uploadRes.statusCode === 401) {
uni.showToast({ title: '登录已失效,请重新登录', icon: 'none' })
break
}
const body = parseUploadBody(uploadRes.data)
const url = extractUrl(body)
if (body.code !== 200 || !url) {
uni.showToast({ title: body.message || '上传失败', icon: 'none' })
break
}
urls.push(url)
}
} catch (e) {
uni.showToast({
title: e instanceof Error ? e.message : '上传失败',
icon: 'none'
})
} finally {
uni.hideLoading()
}
return urls
}
export function previewImages(urls, current) {
const list = (urls || []).map((u) => String(u || '').trim()).filter(Boolean)
if (!list.length) return
uni.previewImage({
urls: list,
current: current || list[0]
})
}