diff --git a/miniprogram/api/bx.js b/miniprogram/api/bx.js
index 1f3e4aeb5..98a81bdff 100644
--- a/miniprogram/api/bx.js
+++ b/miniprogram/api/bx.js
@@ -1,4 +1,4 @@
-import { bxRequest, bxUploadFile } from '@/utils/bxRequest'
+import { bxRequest, bxUploadFile, bxUploadInvoice } from '@/utils/bxRequest'
export const advanceApi = {
list: (params) => bxRequest({ url: '/advances/', method: 'GET', params }),
@@ -34,11 +34,19 @@ export const expenseItemApi = {
export const invoiceWalletApi = {
mineSummary: (params) =>
- bxRequest({ url: '/invoice-wallets/mine/summary', method: 'GET', params })
+ bxRequest({ url: '/invoice-wallets/mine/summary', method: 'GET', params }),
+ list: (params) => bxRequest({ url: '/invoice-wallets', method: 'GET', params })
}
export const invoiceApi = {
- list: (params) => bxRequest({ url: '/invoices', method: 'GET', params })
+ list: (params) => bxRequest({ url: '/invoices', method: 'GET', params }),
+ /**
+ * 上传发票(multipart)。uni-app 用 uni.uploadFile,单次一张图。
+ * @param {{ amount: number|string, remark?: string, filePath: string, targetUserId?: string|number }} opts
+ */
+ upload: (opts) => bxUploadInvoice(opts),
+ delete: (id) => bxRequest({ url: `/invoices/${id}`, method: 'DELETE' }),
+ ledgers: (params) => bxRequest({ url: '/invoice-ledgers', method: 'GET', params })
}
/** 附件:申请页图片上传所需最小集合 */
diff --git a/miniprogram/pages/bx/invoice/admin.vue b/miniprogram/pages/bx/invoice/admin.vue
index 33495d608..22411551c 100644
--- a/miniprogram/pages/bx/invoice/admin.vue
+++ b/miniprogram/pages/bx/invoice/admin.vue
@@ -1,7 +1,612 @@
-
+
+
+
+
+ ‹
+
+
+ {{
+ detailUser ? detailUser.ownerUserName || '发票夹详情' : '发票夹管理'
+ }}
+
+
+
+
+
+
+
+ ⌕
+
+ ×
+
+
+
+
+
+ 暂无数据
+
+
+ {{ idx + 1 }}
+
+ {{ row.ownerUserName || row.ownerUserId }}
+
+ 余额
+ {{ money(row.balance) }}
+
+
+ 总额
+ ¥{{ money(row.totalAmount) }}
+
+
+ 已用
+ ¥{{ money(row.usedAmount) }}
+
+
+ 发票
+ {{ row.invoiceCount ?? 0 }} 张
+
+
+
+
+
+
+
+
+
+
+
+ 总额
+ ¥{{ money(detailSummary.totalAmount) }}
+
+
+ 已用
+ ¥{{ money(detailSummary.usedAmount) }}
+
+
+ 余额
+ ¥{{ money(detailSummary.balance) }}
+
+ 发票 {{ detailSummary.invoiceCount ?? 0 }} 张
+
+
+
+
+
+
+ 暂无发票
+
+
+ {{ idx + 1 }}
+ 金额
+ {{ money(inv.amount) }}
+
+
+ 备注
+ {{ inv.remark || '-' }}
+
+
+ 创建时间
+ {{ inv.createTime || '-' }}
+
+
+ 附件
+
+
+
+
+ ...
+
+
+ {{ fileExt(file.fileName) }}
+ {{ file.fileName }}
+
+
+
+ 暂无附件
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 代传发票
+
+
+ 金额
+
+
+
+
+
+ 备注
+
+
+
+ 发票图片
+ 仅支持图片;PDF 请用管理端。多张时仅上传首张。
+
+
+
+ ×
+
+
+ +
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/bx/invoice/mine.vue b/miniprogram/pages/bx/invoice/mine.vue
index 775992565..855e71828 100644
--- a/miniprogram/pages/bx/invoice/mine.vue
+++ b/miniprogram/pages/bx/invoice/mine.vue
@@ -1,7 +1,579 @@
-
+
+
+
+
+ ‹
+
+
+ 我的发票夹
+
+
+
+
+
+ 总额
+ ¥{{ money(summary.totalAmount) }}
+
+
+ 已报销占用
+ ¥{{ money(summary.usedAmount) }}
+
+
+ 余额
+ ¥{{ money(summary.balance) }}
+
+ 发票 {{ summary.invoiceCount ?? 0 }} 张
+
+
+
+
+
+
+
+
+
+ 发票列表
+
+
+ 额度流水
+
+
+
+
+
+
+ 暂无发票
+
+
+ {{ idx + 1 }}
+ 金额
+ {{ money(inv.amount) }}
+
+
+ 备注
+ {{ inv.remark || '-' }}
+
+
+ 创建时间
+ {{ inv.createTime || '-' }}
+
+
+ 附件
+
+
+
+
+ ...
+
+
+ {{ fileExt(file.fileName) }}
+ {{ file.fileName }}
+
+
+
+ 暂无附件
+
+
+
+
+
+
+ 暂无流水
+
+
+ {{ idx + 1 }}
+
+ {{ entryTypeLabel(row.entryType || row.entry_type) }}
+
+ 金额
+ {{ money(row.amount) }}
+
+
+ 备注
+ {{ ledgerRemark(row) }}
+
+
+ 创建时间
+ {{ row.createTime || row.create_time || '-' }}
+
+
+
+
+
+
+
+
+
+ 上传发票
+
+
+ 金额
+
+
+
+
+
+ 备注
+
+
+
+ 发票图片
+ 仅支持图片;PDF 请用管理端。多张时仅上传首张。
+
+
+
+ ×
+
+
+ +
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/styles/bx-theme.scss b/miniprogram/styles/bx-theme.scss
index 399e5b771..d18af4949 100644
--- a/miniprogram/styles/bx-theme.scss
+++ b/miniprogram/styles/bx-theme.scss
@@ -1283,3 +1283,233 @@
.bx-page .all-list-card__bottom {
justify-content: flex-end;
}
+
+/* —— 发票夹 wallet summary / cards —— */
+.bx-page .wallet-summary.card,
+.bx-page .wallet-summary {
+ margin: 12px 16px 8px;
+ padding: 14px 16px;
+ background: var(--surface);
+ border-radius: var(--r);
+ box-shadow: var(--shadow);
+}
+
+.bx-page .wallet-summary__row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 0;
+ font-size: 14px;
+ color: var(--text-sec);
+}
+
+.bx-page .wallet-summary__val {
+ font-weight: 700;
+ color: var(--text);
+ font-variant-numeric: tabular-nums;
+}
+
+.bx-page .wallet-summary__row--em .wallet-summary__val,
+.bx-page .wallet-summary__row--em b {
+ color: var(--primary);
+ font-size: 18px;
+}
+
+.bx-page .wallet-summary__meta {
+ margin-top: 8px;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.bx-page .wallet-actions {
+ padding: 0 16px 8px;
+}
+
+.bx-page .wallet-list {
+ padding-top: 5px;
+ padding-bottom: 16px;
+ box-sizing: border-box;
+}
+
+.bx-page .invoice-wallet-card {
+ position: relative;
+ overflow: hidden;
+ padding-top: 20px;
+}
+
+.bx-page .invoice-wallet-card--static:active {
+ transform: none;
+}
+
+.bx-page .invoice-wallet-card__index {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ min-width: 28px;
+ height: 22px;
+ padding: 0 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0 0 10px 0;
+ background: var(--primary-dark);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1;
+ font-variant-numeric: tabular-nums;
+ box-sizing: border-box;
+}
+
+.bx-page .invoice-wallet-card__subs {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin: 4px 0 10px;
+}
+
+.bx-page .invoice-wallet-card__sub {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 13px;
+ line-height: 1.4;
+}
+
+.bx-page .invoice-wallet-card__sub .k {
+ flex-shrink: 0;
+ color: var(--text-muted);
+}
+
+.bx-page .invoice-wallet-card__sub .v {
+ min-width: 0;
+ text-align: right;
+ color: var(--text);
+ word-break: break-word;
+}
+
+.bx-page .invoice-wallet-card__att-label {
+ margin-bottom: 6px;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.bx-page .invoice-wallet-card__att-empty {
+ margin: 0;
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.bx-page .advance-att-grid {
+ margin-top: 0;
+}
+
+.bx-page .advance-att-tile:active {
+ opacity: 0.92;
+}
+
+.bx-page .att-preview-loading {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg);
+ color: var(--text-muted);
+ font-size: 12px;
+}
+
+.bx-page .att-file-icon.att-doc-thumb {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 6px 4px;
+ box-sizing: border-box;
+}
+
+.bx-page .att-doc-thumb__ext {
+ font-size: 11px;
+ font-weight: 800;
+ color: var(--primary-dark);
+ letter-spacing: 0.02em;
+}
+
+.bx-page .att-doc-thumb__name {
+ font-size: 10px;
+ color: var(--text-muted);
+ text-align: center;
+ line-height: 1.25;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ word-break: break-word;
+}
+
+.bx-page .invoice-admin-card__bottom {
+ justify-content: flex-end;
+ margin-top: 10px;
+}
+
+.bx-page .empty-state--embed {
+ padding: 28px 16px;
+ min-height: 120px;
+}
+
+.bx-page .att-img.att-add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-style: dashed;
+ color: var(--text-muted);
+ font-size: 28px;
+ line-height: 1;
+}
+
+.bx-page .form-hint {
+ display: block;
+ font-size: 11px;
+ color: var(--text-muted);
+ margin: 0 0 8px;
+ line-height: 1.4;
+}
+
+.bx-page .bx-upload-sheet .dialog-form-body {
+ padding: 8px 16px 4px;
+}
+
+.bx-page .bx-upload-sheet .form-item {
+ margin-bottom: 12px;
+}
+
+.bx-page .bx-upload-sheet .form-label {
+ display: block;
+ font-size: 13px;
+ color: var(--text-sec);
+ margin-bottom: 6px;
+}
+
+.bx-page .bx-upload-sheet .form-input {
+ width: 100%;
+ height: 40px;
+ padding: 0 12px;
+ border: 1px solid var(--border);
+ border-radius: var(--rs);
+ background: var(--bg);
+ box-sizing: border-box;
+ font-size: 14px;
+}
+
+.bx-page .bx-upload-sheet .form-input-prefix {
+ position: relative;
+}
+
+.bx-page .bx-upload-sheet .form-input-prefix .form-input {
+ padding-left: 28px;
+}
diff --git a/miniprogram/utils/bxRequest.js b/miniprogram/utils/bxRequest.js
index 7be078f01..229721a98 100644
--- a/miniprogram/utils/bxRequest.js
+++ b/miniprogram/utils/bxRequest.js
@@ -171,4 +171,73 @@ export function bxUploadFile(filePath, opts = {}) {
})
}
+/**
+ * 发票 multipart 上传(POST /invoices,name=files)
+ * 对齐 bx-mobile FormData:amount / remark / files / targetUserId
+ * @param {{ amount: number|string, remark?: string, filePath: string, targetUserId?: string|number, showError?: boolean }} opts
+ */
+export function bxUploadInvoice(opts = {}) {
+ const showError = opts.showError !== false
+ const token = uni.getStorageSync(TOKEN_KEY) || ''
+ const filePath = opts.filePath
+ if (!filePath) {
+ return Promise.reject(new Error('请选择发票文件'))
+ }
+ const formData = {
+ amount: String(opts.amount ?? '')
+ }
+ if (opts.remark != null && String(opts.remark).trim()) {
+ formData.remark = String(opts.remark).trim()
+ }
+ if (opts.targetUserId != null && opts.targetUserId !== '') {
+ formData.targetUserId = String(opts.targetUserId)
+ }
+ const url = `${API_BASE}/invoices`
+
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url,
+ filePath,
+ name: 'files',
+ formData,
+ header: {
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
+ },
+ success: (res) => {
+ const status = res.statusCode
+ let body = res.data
+ if (typeof body === 'string') {
+ try {
+ body = JSON.parse(body)
+ } catch (e) {
+ body = {}
+ }
+ }
+ if (status === 401) {
+ onUnauthorized()
+ reject(new Error('登录已过期'))
+ return
+ }
+ if (status < 200 || status >= 300) {
+ const message = parseDetailMessage(body) || `上传失败(${status})`
+ if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
+ reject(new Error(message))
+ return
+ }
+ if (body && body.code !== undefined && body.code !== 0) {
+ const message = body.msg || '上传失败'
+ if (showError) uni.showToast({ title: String(message).slice(0, 40), icon: 'none' })
+ reject(new Error(message))
+ return
+ }
+ resolve(body)
+ },
+ fail: (err) => {
+ if (showError) uni.showToast({ title: '上传失败', icon: 'none' })
+ reject(err)
+ }
+ })
+ })
+}
+
export default bxRequest