提交
This commit is contained in:
1 parent
8be55c08e9
commit
a903fcf6d4
75 files changed
+9547
-124
No files matched your search
@@ -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}`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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]
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user