diff --git a/miniprogram/api/bx.js b/miniprogram/api/bx.js
index cf4aaa182..a05cd0a1a 100644
--- a/miniprogram/api/bx.js
+++ b/miniprogram/api/bx.js
@@ -11,7 +11,35 @@ export const advanceApi = {
delete: (id) => bxRequest({ url: `/advances/${id}`, method: 'DELETE' })
}
-/** 附件:Batch2 申请页图片上传所需最小集合 */
+export const reimburseApi = {
+ generateTitle: (params) =>
+ bxRequest({ url: '/reimburses/generate-title', method: 'GET', params }),
+ my: (params) => bxRequest({ url: '/reimburses/my', method: 'GET', params }),
+ getById: (id) => bxRequest({ url: `/reimburses/${id}`, method: 'GET' }),
+ getApprovals: (id) => bxRequest({ url: `/reimburses/${id}/approvals`, method: 'GET' }),
+ getApprovalHistory: (id) =>
+ bxRequest({ url: `/reimburses/${id}/approval-history`, method: 'GET' }),
+ create: (data) => bxRequest({ url: '/reimburses/', method: 'POST', data }),
+ update: (id, data) => bxRequest({ url: `/reimburses/${id}`, method: 'PUT', data }),
+ submit: (id, data) =>
+ bxRequest({ url: `/reimburses/${id}/submit`, method: 'POST', data }),
+ delete: (id) => bxRequest({ url: `/reimburses/${id}`, method: 'DELETE' })
+}
+
+export const expenseItemApi = {
+ tree: () => bxRequest({ url: '/expense-items/tree', method: 'GET' })
+}
+
+export const invoiceWalletApi = {
+ mineSummary: (params) =>
+ bxRequest({ url: '/invoice-wallets/mine/summary', method: 'GET', params })
+}
+
+export const invoiceApi = {
+ list: (params) => bxRequest({ url: '/invoices', method: 'GET', params })
+}
+
+/** 附件:申请页图片上传所需最小集合 */
export const attachmentApi = {
upload: (filePath, bizType = '', bizId = 0) =>
bxUploadFile(filePath, { bizType, bizId }),
@@ -35,4 +63,11 @@ export const attachmentApi = {
})
}
-export default { advanceApi, attachmentApi }
+export default {
+ advanceApi,
+ reimburseApi,
+ expenseItemApi,
+ invoiceWalletApi,
+ invoiceApi,
+ attachmentApi
+}
diff --git a/miniprogram/config/api.js b/miniprogram/config/api.js
index e16fcfb98..23aa370d4 100644
--- a/miniprogram/config/api.js
+++ b/miniprogram/config/api.js
@@ -53,6 +53,8 @@ export const PAGE_PATHS = {
bxAdvanceApplyForm: '/pages/bx/advance/apply',
bxAdvanceDetail: '/pages/bx/advance/detail',
bxReimburseApply: '/pages/bx/reimburse/list',
+ bxReimburseApplyForm: '/pages/bx/reimburse/apply',
+ bxReimburseDetail: '/pages/bx/reimburse/detail',
bxMine: '/pages/bx/mine/list',
bxAll: '/pages/bx/all/list',
bxInvoiceMine: '/pages/bx/invoice/mine',
diff --git a/miniprogram/pages.json b/miniprogram/pages.json
index b86cbbe3f..f74542e18 100644
--- a/miniprogram/pages.json
+++ b/miniprogram/pages.json
@@ -96,6 +96,14 @@
"path": "pages/bx/reimburse/list",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "报销申请" }
},
+ {
+ "path": "pages/bx/reimburse/apply",
+ "style": { "navigationStyle": "custom", "navigationBarTitleText": "报销申请" }
+ },
+ {
+ "path": "pages/bx/reimburse/detail",
+ "style": { "navigationStyle": "custom", "navigationBarTitleText": "报销详情" }
+ },
{
"path": "pages/bx/mine/list",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "我的申请单" }
diff --git a/miniprogram/pages/bx/reimburse/apply.vue b/miniprogram/pages/bx/reimburse/apply.vue
new file mode 100644
index 000000000..ca6e6cb66
--- /dev/null
+++ b/miniprogram/pages/bx/reimburse/apply.vue
@@ -0,0 +1,796 @@
+
+
+
+
+
+ ‹
+
+
+ 报销申请
+
+
+
+
+
+
+
+
+ 申请人
+
+ {{ applicantLabel || '当前用户' }}
+
+
+
+
+ 报销日期*
+
+
+
+ {{ form.reimburseDate || '请选择报销日期' }}
+
+ ›
+
+
+
+
+
+ 报销标题
+
+
+
+
+
+
+
+
+
+
+
+
+ 费用项目
+
+ (第 {{ idx + 1 }} 条)
+
+ *
+
+
+
+ {{ lineExpenseDisplay(line.expenseItemId) || '请选择费用项目' }}
+
+ ›
+
+
+
+
+ 金额(元)*
+
+
+
+
+
+
+ 报销事由*
+
+
+
+
+ 发生时间
+
+
+
+ {{ line.occurDate || '选填' }}
+
+ ›
+
+
+
+
+
+
+
+
+
+
+
+ 合计:¥{{ detailTotalAmount }}
+
+
+
+
+
+
+ 发票余额 ¥{{ money(walletBalance) }} · 本次合计 ¥{{ detailTotalAmount }}
+
+ (余额不足,提交后余额将为负,请后续补发票)
+
+
+
+
+
+
+ {{ pick.fileName }}
+ 整票 ¥{{ money(pick.amount) }}
+
+ ×
+
+
+ 请先在「我的发票夹」上传发票;提交时按合计扣减余额。
+
+
+
+
+
+
+
+
+ ×
+
+
+ 暂无附件(可选)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择费用项目
+
+ 暂无可用费用项目
+
+ {{ opt.label }}
+
+
+
+
+
+
+
+
+ 选择发票附件
+
+ 暂无发票附件
+
+
+ {{ item.fileName }}
+ 整票 ¥{{ money(item.amount) }}
+
+ ✓
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/bx/reimburse/detail.vue b/miniprogram/pages/bx/reimburse/detail.vue
new file mode 100644
index 000000000..51841b583
--- /dev/null
+++ b/miniprogram/pages/bx/reimburse/detail.vue
@@ -0,0 +1,391 @@
+
+
+
+
+
+ ‹
+
+
+ 报销详情
+
+
+
+
+
+
+
+ {{ detail.applyCode || '—' }}
+
+ {{ statusLabel(detail.status) }}
+
+
+
+
+
+
+ {{ detail.title || '—' }}
+ {{ formatAmount(detail.amount) }}
+
+
+
+
+
+
+ 基础信息
+
+
+ 审批信息
+
+
+
+
+
+
+ 申请单号
+ {{ detail.applyCode || '—' }}
+
+
+ 报销标题
+ {{ detail.title || '—' }}
+
+
+ 申请人
+ {{ detail.applicantName || '—' }}
+
+
+ 冲抵金额
+
+ {{ moneyOrDash(detail.offsetAmount ?? detail.offset_amount) }}
+
+
+
+ 实付金额
+
+ {{
+ moneyOrDash(
+ detail.payAmount ?? detail.actualAmount ?? detail.actual_amount
+ )
+ }}
+
+
+
+ 当前节点
+ {{ detail.currentNode || '—' }}
+
+
+ 申请时间
+ {{ formatApplyDateTime(detail) }}
+
+
+ 报销说明
+
+ {{ detail.remark || '—' }}
+
+
+
+
+
+ 费用项信息
+
+
+ 明细 {{ idx + 1 }}
+
+
+ 费用项目
+ {{ resolveExpenseDisplay(d, flatList) }}
+
+
+ 金额
+
+ ¥ {{ formatAmount(d.amount) }}
+
+
+
+ 报销事由
+ {{ d.remark }}
+
+
+ 发生时间
+ {{ d.occurDate || '—' }}
+
+
+
+ 合计
+
+ ¥ {{ totalAmount }}
+
+
+
+
+
+ 申请附件
+
+
+
+
+ 文件
+
+
+
+ 暂无申请附件
+
+
+
+
+
+
+
+
+ {{ (a.approverName || a.nodeName || a.nodeLabel || '审')[0] }}
+
+
+
+ {{ a.nodeName || a.nodeLabel || a.approverName || '—' }}
+
+ {{ a.approverName }}
+
+ {{ formatDateTime(a.approvalTime) }}
+
+
+ {{ a.approvalOpinion }}
+
+
+
+
+ 暂无审批记录
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/bx/reimburse/list.vue b/miniprogram/pages/bx/reimburse/list.vue
index 2e3e06e65..ea2814a5d 100644
--- a/miniprogram/pages/bx/reimburse/list.vue
+++ b/miniprogram/pages/bx/reimburse/list.vue
@@ -1,7 +1,326 @@
-
+
+
+
+
+ ‹
+
+
+ 报销申请
+
+
+
+ +
+
+
+
+ ⌕
+
+ ×
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+ 暂无报销记录
+
+
+
+
+
+ {{ cardTitle(item) }}
+
+
+ {{ statusLabel(item.status) }}
+
+
+ 报销金额
+ {{ formatAmount(item.amount) }}
+
+
+ 冲抵金额
+ ¥ {{ moneyOrDash(item.offsetAmount ?? item.offset_amount) }}
+
+
+ 实付金额
+
+ ¥ {{ moneyOrDash(item.payAmount ?? item.actualAmount ?? item.actual_amount) }}
+
+
+
+
+
+ 申请单号
+ {{ item.applyCode || '—' }}
+
+
+ 申请人
+ {{ item.applicantName || '-' }}
+
+
+ 费用项目
+ {{ resolveExpenseDisplay(item, flatList) }}
+
+
+ 当前环节
+ {{ item.currentNode }}
+
+
+ 发票 / 附件
+
+ {{ item.invoiceCount ?? 0 }} 张 · {{ item.attachCount ?? 0 }} 个
+
+
+
+ 备注
+ {{ item.remark }}
+
+
+
+ {{ formatApplyDateTime(item) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/styles/bx-theme.scss b/miniprogram/styles/bx-theme.scss
index fc0c4dfde..780456a38 100644
--- a/miniprogram/styles/bx-theme.scss
+++ b/miniprogram/styles/bx-theme.scss
@@ -255,7 +255,8 @@
line-height: 1.3;
}
-.bx-page .advance-list-card__title {
+.bx-page .advance-list-card__title,
+.bx-page .reimburse-list-card__title {
display: block;
overflow: hidden;
text-overflow: ellipsis;
@@ -340,7 +341,8 @@
margin-right: 2px;
}
-.bx-page .advance-card-subs {
+.bx-page .advance-card-subs,
+.bx-page .reimburse-card-subs {
display: flex;
flex-direction: column;
gap: 6px;
@@ -349,7 +351,8 @@
border-top: 1px solid var(--border);
}
-.bx-page .advance-card-sub-row {
+.bx-page .advance-card-sub-row,
+.bx-page .reimburse-card-sub-row {
display: flex;
justify-content: space-between;
align-items: center;
@@ -357,13 +360,15 @@
line-height: 1.35;
}
-.bx-page .advance-card-sub-row .k {
+.bx-page .advance-card-sub-row .k,
+.bx-page .reimburse-card-sub-row .k {
color: var(--text-sec);
flex-shrink: 0;
margin-right: 8px;
}
-.bx-page .advance-card-sub-row .v {
+.bx-page .advance-card-sub-row .v,
+.bx-page .reimburse-card-sub-row .v {
font-weight: 600;
color: var(--text);
font-variant-numeric: tabular-nums;
@@ -995,3 +1000,235 @@
line-height: 1.45;
word-break: break-word;
}
+
+/* Reimburse apply: detail lines + pickers */
+.bx-page .detail-line-no {
+ font-weight: 500;
+ color: var(--text-muted);
+ font-size: 12px;
+}
+
+.bx-page .detail-line .detail-divider {
+ height: 1px;
+ background: var(--border);
+ margin: 12px 0;
+}
+
+.bx-page .detail-line-actions {
+ text-align: right;
+ margin-top: 4px;
+}
+
+.bx-page .detail-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px dashed var(--border);
+ gap: 8px;
+}
+
+.bx-page .total-amount {
+ font-size: 14px;
+ color: var(--text-sec);
+ flex-shrink: 0;
+}
+
+.bx-page .total-amount b {
+ color: var(--primary-dark);
+ font-size: 16px;
+}
+
+.bx-page .picker-trigger {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 42px;
+ padding: 8px 12px;
+ border: 1.5px solid var(--border);
+ border-radius: var(--rs);
+ background: var(--surface);
+ box-sizing: border-box;
+}
+
+.bx-page .picker-trigger:active {
+ background: var(--bg);
+}
+
+.bx-page .picker-trigger .value {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.bx-page .picker-trigger .ph {
+ color: var(--text-muted);
+ font-weight: 600;
+}
+
+.bx-page .picker-trigger .arrow {
+ color: var(--text-muted);
+ flex-shrink: 0;
+ margin-left: 8px;
+ font-size: 16px;
+}
+
+.bx-page .form-tip {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin: 8px 0 0;
+ line-height: 1.45;
+}
+
+.bx-page .invoice-balance-tip {
+ font-size: 13px;
+ color: var(--text-sec);
+ margin: 0 0 10px;
+ line-height: 1.5;
+}
+
+.bx-page .invoice-balance-tip--warn {
+ color: #dc2626;
+ margin-left: 4px;
+}
+
+.bx-page .invoice-picked-row {
+ margin-top: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.bx-page .invoice-picked-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: var(--rs);
+ background: var(--bg);
+}
+
+.bx-page .invoice-picked-item__meta {
+ flex: 1;
+ min-width: 0;
+}
+
+.bx-page .invoice-picked-item__name {
+ display: block;
+ font-size: 13px;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.bx-page .invoice-picked-item__amt {
+ font-size: 12px;
+ color: var(--text-sec);
+ margin-top: 2px;
+}
+
+.bx-page .invoice-picked-item__remove {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ line-height: 28px;
+ text-align: center;
+ border-radius: 50%;
+ background: rgba(15, 23, 42, 0.45);
+ color: #fff;
+ font-size: 16px;
+}
+
+.bx-page .bx-sheet-mask {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgba(15, 23, 42, 0.45);
+ display: flex;
+ align-items: flex-end;
+}
+
+.bx-page .bx-sheet {
+ width: 100%;
+ max-height: 70vh;
+ background: var(--surface);
+ border-radius: 16px 16px 0 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.bx-page .bx-sheet__title {
+ padding: 14px 16px;
+ font-size: 16px;
+ font-weight: 700;
+ border-bottom: 1px solid var(--border);
+ text-align: center;
+}
+
+.bx-page .bx-sheet__body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 0;
+ max-height: 50vh;
+}
+
+.bx-page .bx-sheet__item {
+ padding: 12px 16px;
+ font-size: 14px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.bx-page .bx-sheet__item.is-selected {
+ background: var(--primary-bg);
+ color: var(--primary-dark);
+ font-weight: 600;
+}
+
+.bx-page .bx-sheet__item-sub {
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+
+.bx-page .bx-sheet__footer {
+ padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
+ border-top: 1px solid var(--border);
+}
+
+.bx-page .bx-sheet__empty {
+ padding: 28px 16px;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 13px;
+}
+
+.bx-page .reimburse-detail-group + .reimburse-detail-group {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px dashed var(--border);
+}
+
+.bx-page .reimburse-detail-head {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-muted);
+ margin-bottom: 8px;
+}
+
+.bx-page .reimburse-detail-total-row {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+}
diff --git a/miniprogram/utils/bxFormat.js b/miniprogram/utils/bxFormat.js
index 28dd7bed1..0c6ab5854 100644
--- a/miniprogram/utils/bxFormat.js
+++ b/miniprogram/utils/bxFormat.js
@@ -85,3 +85,67 @@ export function formatApplyDateTime(row) {
export function formatApplyDate(row) {
return formatDate(pickApplyTime(row))
}
+
+/** 费用树扁平化(tree → flatList) */
+export function flattenExpenseTree(tree) {
+ const result = []
+ for (const node of tree || []) {
+ result.push(node)
+ if (node.children?.length) result.push(...flattenExpenseTree(node.children))
+ }
+ return result
+}
+
+export function normalizeExpenseItem(raw) {
+ const children = Array.isArray(raw?.children)
+ ? raw.children.map(normalizeExpenseItem)
+ : undefined
+ return {
+ id: String(raw?.id ?? ''),
+ parentId: raw?.parentId == null || raw?.parentId === '' ? '0' : String(raw.parentId),
+ itemName: raw?.itemName ?? '',
+ itemCode: raw?.itemCode ?? '',
+ level: Number(raw?.level ?? 0),
+ sortOrder: Number(raw?.sortOrder ?? 0),
+ status: raw?.status,
+ childCount: Number(raw?.childCount ?? children?.length ?? 0),
+ children
+ }
+}
+
+export function formatExpenseName(id, flatList) {
+ if (id == null || id === '') return '-'
+ const sid = String(id)
+ const parts = []
+ let cur = (flatList || []).find((i) => String(i.id) === sid)
+ const guard = new Set()
+ while (cur && !guard.has(cur.id)) {
+ guard.add(cur.id)
+ parts.unshift(cur.itemName)
+ const pid = cur.parentId
+ cur =
+ pid != null && pid !== '' && pid !== '0'
+ ? (flatList || []).find((i) => String(i.id) === pid)
+ : undefined
+ }
+ return parts.length ? parts.join('/') : '-'
+}
+
+export function pickExpenseItemId(row) {
+ if (!row || typeof row !== 'object') return null
+ const raw = row.expenseId ?? row.expense_id ?? row.expenseItemId ?? row.expense_item_id
+ if (raw == null || raw === '') return null
+ return String(raw)
+}
+
+export function resolveExpenseDisplay(row, flatList) {
+ if (!row) return '-'
+ const id = pickExpenseItemId(row)
+ const nameFb = row.expenseName ?? row.expense_name
+ if (id != null && flatList && flatList.length) {
+ const p = formatExpenseName(id, flatList)
+ if (p && p !== '-') return p
+ }
+ const fb = nameFb != null ? String(nameFb).trim() : ''
+ return fb || '-'
+}