diff --git a/miniprogram/api/bx.js b/miniprogram/api/bx.js
index 98a81bdff..91481a5fc 100644
--- a/miniprogram/api/bx.js
+++ b/miniprogram/api/bx.js
@@ -1,4 +1,5 @@
import { bxRequest, bxUploadFile, bxUploadInvoice } from '@/utils/bxRequest'
+import { request, parseResponseJson } from '@/utils/request'
export const advanceApi = {
list: (params) => bxRequest({ url: '/advances/', method: 'GET', params }),
@@ -29,7 +30,41 @@ export const reimburseApi = {
}
export const expenseItemApi = {
- tree: () => bxRequest({ url: '/expense-items/tree', method: 'GET' })
+ tree: () => bxRequest({ url: '/expense-items/tree', method: 'GET' }),
+ create: (data) => bxRequest({ url: '/expense-items/', method: 'POST', data }),
+ update: (id, data) => bxRequest({ url: `/expense-items/${id}`, method: 'PUT', data }),
+ delete: (id) => bxRequest({ url: `/expense-items/${id}`, method: 'DELETE' })
+}
+
+export const workflowApi = {
+ getByBizType: (bizType) =>
+ bxRequest({ url: `/workflows/biz/${bizType}`, method: 'GET' }),
+ create: (data) => bxRequest({ url: '/workflows/', method: 'POST', data }),
+ update: (id, data) => bxRequest({ url: `/workflows/${id}`, method: 'PUT', data }),
+ saveNodes: (id, nodes) =>
+ bxRequest({ url: `/workflows/${id}/nodes`, method: 'PUT', data: nodes }),
+ delete: (id) => bxRequest({ url: `/workflows/${id}`, method: 'DELETE' })
+}
+
+/**
+ * 门户用户下拉(code===200 信封,非 bx code===0)
+ * 对齐 bx-mobile portalAdminApi.userOptions → GET /web/admin/users/options
+ */
+export async function fetchPortalUserOptions() {
+ const res = await request({
+ url: '/web/admin/users/options',
+ method: 'GET'
+ })
+ const result = parseResponseJson(res)
+ if (result && typeof result === 'object' && 'code' in result) {
+ if (result.code === 200 || result.code === 0) {
+ const data = result.data !== undefined ? result.data : result
+ return Array.isArray(data) ? data : []
+ }
+ const msg = result.message || result.msg || '加载用户失败'
+ throw new Error(msg)
+ }
+ return Array.isArray(result) ? result : []
}
export const invoiceWalletApi = {
@@ -77,7 +112,9 @@ export default {
advanceApi,
reimburseApi,
expenseItemApi,
+ workflowApi,
invoiceWalletApi,
invoiceApi,
- attachmentApi
+ attachmentApi,
+ fetchPortalUserOptions
}
diff --git a/miniprogram/components/bx-ui/bx-wf-config-page.vue b/miniprogram/components/bx-ui/bx-wf-config-page.vue
new file mode 100644
index 000000000..717e93753
--- /dev/null
+++ b/miniprogram/components/bx-ui/bx-wf-config-page.vue
@@ -0,0 +1,466 @@
+
+
+
+
+
+ ‹
+
+
+ {{ pageTitle }}
+
+
+
+ +
+
+
+
+
+ 流程名称
+ {{ wfDef.wfName || '-' }}
+
+
+ 状态
+
+
+ {{ statusLabel(wfDef.status) }}
+
+
+
+
+
+
+ 加载中...
+
+
+
+ 审批节点
+
+
+
+ {{ index + 1 }}
+
+
+ {{ node.nodeName }}
+
+ {{ node.approverId || node.approver_id ? '已配置' : '未配置' }}
+
+
+ 审批人:{{ node.approverName ?? node.approver_name ?? '-' }}
+ 节点顺序:{{ node.nodeOrder }}
+
+
+
+
+
+
+
+
+
+
+
+ 暂无流程节点,点击右下角新增
+
+
+
+
+
+
+
+ {{ isEdit ? '编辑节点' : '新增节点' }}
+
+
+
+ 节点名称*
+
+
+
+ 审批人
+
+ {{
+ approverDisplay || '请选择审批人'
+ }}
+ ›
+
+
+ 用户列表不可用时,可手动填写审批人 ID / 姓名
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择审批人
+
+
+ ⌕
+
+ ×
+
+
+
+ 无匹配人员
+
+ {{ u.displayName }}
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/bx/admin/expense-item.vue b/miniprogram/pages/bx/admin/expense-item.vue
index 963fb45c9..c5487b72e 100644
--- a/miniprogram/pages/bx/admin/expense-item.vue
+++ b/miniprogram/pages/bx/admin/expense-item.vue
@@ -1,7 +1,431 @@
-
+
+
+
+
+ ‹
+
+
+ 费用项目管理
+
+
+
+ +
+
+
+
+ 加载中...
+
+
+
+ 费用项目
+
+
+
+
+ ›
+ {{ item.itemName }}
+
+
+ 一级
+
+ {{ statusLabel(item.status) }}
+
+
+
+
+
+
+
+ 项目编码
+ {{ item.itemCode || '-' }}
+
+
+ 排序号
+ {{ item.sortOrder }}
+
+
+ 子项数量
+ {{
+ item.childCount ?? (item.children && item.children.length) ?? 0
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.itemName }}/{{ child.itemName }}
+
+ 二级
+
+ {{ statusLabel(child.status) }}
+
+
+
+
+ 编码:{{ child.itemCode || '-' }}
+ 排序:{{ child.sortOrder }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无费用项目数据
+
+
+
+
+
+
+ {{ dialogTitle }}
+
+
+
+ 项目名称*
+
+
+
+ 项目编码
+
+
+
+ 上级项目
+
+ {{
+ parentDisplay || '请选择上级项目'
+ }}
+ ›
+
+
+
+ 排序号
+
+
+
+ 备注
+
+ {{ (form.remark || '').length }}/200
+
+
+ 状态
+
+
+ {{ form.status === 1 ? '启用' : '禁用' }}
+
+
+
+
+
+
+
+
+
+
+
+ 选择上级项目
+
+ 暂无一级项目
+
+ {{ p.itemName }}
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/bx/admin/wf-advance.vue b/miniprogram/pages/bx/admin/wf-advance.vue
index 7b952015e..0074219a9 100644
--- a/miniprogram/pages/bx/admin/wf-advance.vue
+++ b/miniprogram/pages/bx/admin/wf-advance.vue
@@ -1,7 +1,7 @@
-
+
diff --git a/miniprogram/pages/bx/admin/wf-reimburse.vue b/miniprogram/pages/bx/admin/wf-reimburse.vue
index d2a4b29c6..5c4c49ae2 100644
--- a/miniprogram/pages/bx/admin/wf-reimburse.vue
+++ b/miniprogram/pages/bx/admin/wf-reimburse.vue
@@ -1,7 +1,7 @@
-
+
diff --git a/miniprogram/styles/bx-theme.scss b/miniprogram/styles/bx-theme.scss
index d18af4949..3f7813bc7 100644
--- a/miniprogram/styles/bx-theme.scss
+++ b/miniprogram/styles/bx-theme.scss
@@ -1513,3 +1513,257 @@
.bx-page .bx-upload-sheet .form-input-prefix .form-input {
padding-left: 28px;
}
+
+/* ---- Batch6: expense tree + workflow config ---- */
+.bx-page .section-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 14px 0 10px;
+ font-size: 14px;
+ font-weight: 800;
+ color: var(--text);
+}
+
+.bx-page .section-title::before {
+ content: '';
+ width: 3px;
+ height: 14px;
+ border-radius: 2px;
+ background: var(--primary);
+ flex-shrink: 0;
+}
+
+.bx-page .card-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding-bottom: 72px;
+}
+
+.bx-page .tree-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+}
+
+.bx-page .tree-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 12px 14px;
+ background: linear-gradient(135deg, var(--primary-bg), #fff);
+}
+
+.bx-page .tree-head__left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ flex: 1;
+}
+
+.bx-page .tree-head__title {
+ font-size: 14px;
+ font-weight: 800;
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.bx-page .tree-head__tags {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.bx-page .tree-twisty {
+ width: 22px;
+ height: 22px;
+ border-radius: 7px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--primary-dark);
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 1;
+ transition: transform 0.15s;
+ flex-shrink: 0;
+ transform: rotate(0deg);
+}
+
+.bx-page .tree-twisty.expanded {
+ transform: rotate(90deg);
+}
+
+.bx-page .tree-body {
+ border-top: 1px solid var(--border);
+}
+
+.bx-page .tree-meta-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 6px;
+ padding: 8px 14px;
+ background: var(--bg);
+}
+
+.bx-page .tree-meta-cell .k {
+ display: block;
+ font-size: 11px;
+ color: var(--text-muted);
+}
+
+.bx-page .tree-meta-cell .v {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text);
+ margin-top: 2px;
+ word-break: break-all;
+}
+
+.bx-page .tree-child-card {
+ margin: 0 14px 10px;
+ border-left: 3px solid var(--primary);
+}
+
+.bx-page .op-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ box-shadow: var(--shadow);
+ padding: 14px;
+}
+
+.bx-page .op-card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.bx-page .op-card-title {
+ font-size: 14px;
+ font-weight: 800;
+ line-height: 1.3;
+ flex: 1;
+ min-width: 0;
+}
+
+.bx-page .op-card-sub {
+ margin-top: 4px;
+ font-size: 12px;
+ color: var(--text-muted);
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.bx-page .op-card-body {
+ padding: 10px 0 0;
+}
+
+.bx-page .tree-body > .op-card-body {
+ padding: 10px 14px;
+}
+
+.bx-page .tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ border-radius: 8px;
+ font-size: 11px;
+ font-weight: 600;
+ border: 1px solid transparent;
+ line-height: 1.25;
+}
+
+.bx-page .tag-neutral {
+ background: #f1f5f9;
+ color: #64748b;
+ border-color: rgba(100, 116, 139, 0.15);
+}
+
+.bx-page .detail-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.bx-page .detail-item {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--rs);
+ padding: 10px 12px;
+}
+
+.bx-page .detail-item .di-label {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin-bottom: 2px;
+}
+
+.bx-page .detail-item .di-value {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text);
+ word-break: break-all;
+}
+
+.bx-page .flow-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--r);
+ padding: 16px;
+ margin-bottom: 72px;
+}
+
+.bx-page .flow-info__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.bx-page .form-textarea-counter {
+ margin-top: 4px;
+ text-align: right;
+ font-size: 11px;
+ color: var(--text-muted);
+}
+
+.bx-page .switch-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.bx-page .switch-row__label {
+ font-size: 13px;
+ color: var(--text-sec);
+}
+
+.bx-page .field-tip {
+ margin-top: 6px;
+ font-size: 11px;
+ color: var(--warning);
+ line-height: 1.4;
+}
+
+.bx-page .bx-sheet--tall {
+ max-height: 85vh;
+}
+
+.bx-page .bx-sheet__body--form {
+ padding: 8px 16px 4px;
+ max-height: 60vh;
+}
diff --git a/miniprogram/utils/bxFormat.js b/miniprogram/utils/bxFormat.js
index 0c6ab5854..f99190664 100644
--- a/miniprogram/utils/bxFormat.js
+++ b/miniprogram/utils/bxFormat.js
@@ -38,6 +38,26 @@ export function statusTone(status) {
return map[status] ?? 'info'
}
+/** StatusTag variant(对齐 bx-mobile statusTagClass) */
+export function statusTagClass(status) {
+ const tone = statusTone(status)
+ const map = {
+ info: 'info',
+ warn: 'warning',
+ done: 'success',
+ error: 'danger',
+ processing: 'processing'
+ }
+ return map[tone] || 'info'
+}
+
+/** 小程序 status-tag class:启用/禁用或工作流状态 */
+export function adminStatusTagClass(status) {
+ if (status === 1 || status === '1') return 's-approved'
+ if (status === 0 || status === '0') return 's-draft'
+ return workflowListStatusTagClass(status)
+}
+
export function workflowListStatusTagClass(status) {
const raw = status == null || status === '' ? 'draft' : String(status)
return `s-${raw}`