feat(miniprogram): add mobile bx menus and Batch1 placeholders.

Insert MINI_BX_* menus with mini:bx:* perms on test/prod, expose 9 workbench entries, and register placeholder pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-11 23:12:55 +08:00
1 parent 279e405149
commit 507823dfce
21 files changed
+391 -1453

No files matched your search

@@ -0,0 +1,200 @@
-- 小程序报销:移动端菜单 + 超级管理员授权(Batch 1)
-- 设计: docs/superpowers/specs/2026-09-11-mini-bx-from-bx-mobile-design.md
--
-- 结构:
-- 移动端 (id=1)
-- └─ 报销管理 (MINI_BX_ROOT, M)
-- ├─ 备用金申请 mini:bx:advanceApply:list
-- ├─ 报销申请 mini:bx:reimburseApply:list
-- ├─ 我的申请单 mini:bx:viewMine:list
-- ├─ 全部申请单 mini:bx:viewAll:list
-- ├─ 我的发票夹 mini:bx:invoiceWallet:mine
-- ├─ 发票夹管理 mini:bx:invoiceWallet:admin
-- ├─ 费用项目 mini:bx:expenseItem:list
-- ├─ 工作流(备用金) mini:bx:workflowAdvance:list
-- └─ 工作流(报销) mini:bx:workflowReimburse:list
--
-- 授权:仅 role_name='超级管理员';其它角色在角色管理手工勾选。
-- 执行环境:测试库(backend/.env)与生产库(backend/.env.production)数据库名均为 shanxia2.0。
SET NAMES utf8mb4;
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_ROOT', '报销管理', '1', 'M', '/mini/bx', NULL, NULL,
1, '1', '1', 'shanxia2.0', 2, '小程序报销模块(自 bx-mobile 迁移)',
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_ROOT' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_ADVANCE_APPLY', '备用金申请', 'MINI_BX_ROOT', 'C', 'advance-apply', NULL, 'mini:bx:advanceApply:list',
1, '1', '1', 'shanxia2.0', 1, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_ADVANCE_APPLY' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_REIMBURSE_APPLY', '报销申请', 'MINI_BX_ROOT', 'C', 'reimburse-apply', NULL, 'mini:bx:reimburseApply:list',
1, '1', '1', 'shanxia2.0', 2, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_REIMBURSE_APPLY' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_VIEW_MINE', '我的申请单', 'MINI_BX_ROOT', 'C', 'mine', NULL, 'mini:bx:viewMine:list',
1, '1', '1', 'shanxia2.0', 3, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_VIEW_MINE' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_VIEW_ALL', '全部申请单', 'MINI_BX_ROOT', 'C', 'all', NULL, 'mini:bx:viewAll:list',
1, '1', '1', 'shanxia2.0', 4, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_VIEW_ALL' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_INVOICE_MINE', '我的发票夹', 'MINI_BX_ROOT', 'C', 'invoice-mine', NULL, 'mini:bx:invoiceWallet:mine',
1, '1', '1', 'shanxia2.0', 5, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_INVOICE_MINE' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_INVOICE_ADMIN', '发票夹管理', 'MINI_BX_ROOT', 'C', 'invoice-admin', NULL, 'mini:bx:invoiceWallet:admin',
1, '1', '1', 'shanxia2.0', 6, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_INVOICE_ADMIN' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_EXPENSE_ITEM', '费用项目', 'MINI_BX_ROOT', 'C', 'expense-item', NULL, 'mini:bx:expenseItem:list',
1, '1', '1', 'shanxia2.0', 7, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_EXPENSE_ITEM' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_WF_ADVANCE', '工作流(备用金)', 'MINI_BX_ROOT', 'C', 'wf-advance', NULL, 'mini:bx:workflowAdvance:list',
1, '1', '1', 'shanxia2.0', 8, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_WF_ADVANCE' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'MINI_BX_WF_REIMBURSE', '工作流(报销)', 'MINI_BX_ROOT', 'C', 'wf-reimburse', NULL, 'mini:bx:workflowReimburse:list',
1, '1', '1', 'shanxia2.0', 9, NULL,
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_BX_WF_REIMBURSE' AND `del_flag` = 0
);
INSERT INTO `sys_role_menu` (
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
MD5(CONCAT(r.id, '|', m.menu_id, '|mini-bx')),
r.id,
m.menu_id,
r.tenant_no,
0,
'mini bx menus',
'system',
'',
NOW(),
NULL,
NOW(),
0
FROM `sys_role` r
INNER JOIN (
SELECT 'MINI_BX_ROOT' AS menu_id
UNION ALL SELECT 'MINI_BX_ADVANCE_APPLY'
UNION ALL SELECT 'MINI_BX_REIMBURSE_APPLY'
UNION ALL SELECT 'MINI_BX_VIEW_MINE'
UNION ALL SELECT 'MINI_BX_VIEW_ALL'
UNION ALL SELECT 'MINI_BX_INVOICE_MINE'
UNION ALL SELECT 'MINI_BX_INVOICE_ADMIN'
UNION ALL SELECT 'MINI_BX_EXPENSE_ITEM'
UNION ALL SELECT 'MINI_BX_WF_ADVANCE'
UNION ALL SELECT 'MINI_BX_WF_REIMBURSE'
) m
WHERE r.role_name = '超级管理员'
AND r.tenant_no = 'shanxia2.0'
AND r.del_flag = 0
AND NOT EXISTS (
SELECT 1 FROM `sys_role_menu` x
WHERE x.role_id = r.id
AND x.menu_id = m.menu_id
AND x.del_flag = 0
);
@@ -0,0 +1,29 @@
# 小程序报销 Batch1:菜单权限 + 占位入口 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在移动端挂「报销管理」9 个叶子菜单(`mini:bx:*`),测产双库执行;工作台露出入口并进入占位页。
**Architecture:** 幂等 SQL(对齐 MINI_DB_* 模式)+ workbench-menus perms + PAGE_PATHS/ROUTE_MAP + 9 个占位 vue(mp-shell 风格,后续批次再上 vant-weapp 业务页)。
**Tech Stack:** MySQL、uni-app、现有 menu-codes 过滤
**Spec:** `docs/superpowers/specs/2026-09-11-mini-bx-from-bx-mobile-design.md` § Batch 1
---
## 文件结构
| 操作 | 文件 |
|------|------|
| Create | `docs/sql/migrations/2026-09-11-mini-bx-menus.sql` |
| Create | `miniprogram/pages/bx/*/placeholder.vue`(9 页,可共用一个组件模板) |
| Modify | `miniprogram/config/workbench-menus.js` |
| Modify | `miniprogram/config/api.js` |
| Modify | `miniprogram/pages.json` |
### Task 1: SQL + 双库执行
### Task 2: workbench menus + routes + pages.json + 9 placeholders
### Task 3: 验收过滤与提交
(详细步骤见执行会话;SQL 内容以 design 表为准。)
@@ -0,0 +1,49 @@
<script setup>
import { computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { PAGE_PATHS } from '@/config/api'
const props = defineProps({
defaultTitle: { type: String, default: '报销业务' }
})
const title = computed(() => props.defaultTitle)
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
onLoad(() => {})
</script>
<template>
<view class="mp-page">
<view class="mp-header">
<view class="bx-ph__nav" @click="goBack">
<text class="bx-ph__back">← 工作台</text>
</view>
<text class="mp-header__title" style="margin-top: 16rpx">{{ title }}</text>
<text class="mp-header__meta">报销管理 · 业务迁移中</text>
</view>
<view class="mp-placeholder">
<view class="mp-placeholder__icon" />
<text class="mp-placeholder__title">{{ title }}</text>
<text class="mp-placeholder__desc">
菜单与权限已开通。列表 / 表单 / 详情将按批次从 bx-mobile 迁入,接口复用现有报销 API。
</text>
</view>
</view>
</template>
<style lang="scss" scoped>
.bx-ph__nav {
min-height: 44rpx;
}
.bx-ph__back {
color: #fff;
font-size: 26rpx;
opacity: 0.95;
}
</style>
+20 -2
View File
@@ -48,7 +48,16 @@ export const PAGE_PATHS = {
dbInventoryList: '/pages/db/inventory/list',
dbInventoryDetail: '/pages/db/inventory/detail',
dbWeighingList: '/pages/db/weighing/list',
dbWeighingDetail: '/pages/db/weighing/detail'
dbWeighingDetail: '/pages/db/weighing/detail',
bxAdvanceApply: '/pages/bx/advance/list',
bxReimburseApply: '/pages/bx/reimburse/list',
bxMine: '/pages/bx/mine/list',
bxAll: '/pages/bx/all/list',
bxInvoiceMine: '/pages/bx/invoice/mine',
bxInvoiceAdmin: '/pages/bx/invoice/admin',
bxExpenseItem: '/pages/bx/admin/expense-item',
bxWfAdvance: '/pages/bx/admin/wf-advance',
bxWfReimburse: '/pages/bx/admin/wf-reimburse'
}
/** 工作台菜单 code → 页面路径 */
@@ -56,7 +65,16 @@ export const WORKBENCH_ROUTE_MAP = {
'db-dashboard': PAGE_PATHS.dbDashboard,
'db-plans': PAGE_PATHS.dbPlansList,
'db-inventory': PAGE_PATHS.dbInventoryList,
'db-weighing': PAGE_PATHS.dbWeighingList
'db-weighing': PAGE_PATHS.dbWeighingList,
'bx-advance-apply': PAGE_PATHS.bxAdvanceApply,
'bx-reimburse-apply': PAGE_PATHS.bxReimburseApply,
'bx-mine': PAGE_PATHS.bxMine,
'bx-all': PAGE_PATHS.bxAll,
'bx-invoice-mine': PAGE_PATHS.bxInvoiceMine,
'bx-invoice-admin': PAGE_PATHS.bxInvoiceAdmin,
'bx-expense-item': PAGE_PATHS.bxExpenseItem,
'bx-wf-advance': PAGE_PATHS.bxWfAdvance,
'bx-wf-reimburse': PAGE_PATHS.bxWfReimburse
}
export const TOKEN_KEY = 'sx_token'
+11 -11
View File
@@ -25,19 +25,19 @@ export const WORKBENCH_MODULES = [
{
key: 'bx',
title: '报销管理',
desc: '借款 · 报销 · 审批 · 付款',
desc: '备用金 · 报销 · 发票 · 配置',
iconText: '报销',
visible: false,
visible: true,
menus: [
{ name: '待办任务', enabled: false, code: 'bx-todo', icon: '📝' },
{ name: '报销申请', enabled: false, code: 'bx-reimburse-apply', icon: '🧾' },
{ name: '报销审批', enabled: false, code: 'bx-reimburse-approve', icon: '✔️' },
{ name: '借款申请', enabled: false, code: 'bx-advance-apply', icon: '💳' },
{ name: '借款审批', enabled: false, code: 'bx-advance-approve', icon: '🔍' },
{ name: '借款发放', enabled: false, icon: '💸' },
{ name: '我的单据', enabled: false, code: 'bx-mine', icon: '📋' },
{ name: '全部单据', enabled: false, icon: '📚' },
{ name: '付款记录', enabled: false, icon: '🏦' }
{ name: '备用金申请', enabled: true, code: 'bx-advance-apply', icon: '💳', perms: 'mini:bx:advanceApply:list' },
{ name: '报销申请', enabled: true, code: 'bx-reimburse-apply', icon: '🧾', perms: 'mini:bx:reimburseApply:list' },
{ name: '我的申请单', enabled: true, code: 'bx-mine', icon: '📋', perms: 'mini:bx:viewMine:list' },
{ name: '全部申请单', enabled: true, code: 'bx-all', icon: '📚', perms: 'mini:bx:viewAll:list' },
{ name: '我的发票夹', enabled: true, code: 'bx-invoice-mine', icon: '📷', perms: 'mini:bx:invoiceWallet:mine' },
{ name: '发票夹管理', enabled: true, code: 'bx-invoice-admin', icon: '🗂️', perms: 'mini:bx:invoiceWallet:admin' },
{ name: '费用项目', enabled: true, code: 'bx-expense-item', icon: '🌳', perms: 'mini:bx:expenseItem:list' },
{ name: '工作流(备用金)', enabled: true, code: 'bx-wf-advance', icon: '⚙️', perms: 'mini:bx:workflowAdvance:list' },
{ name: '工作流(报销)', enabled: true, code: 'bx-wf-reimburse', icon: '🔧', perms: 'mini:bx:workflowReimburse:list' }
]
},
{
+25 -120
View File
@@ -52,98 +52,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/crm/product-category/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/product-category/form",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/clue/list",
"style": { "navigationStyle": "custom", "enablePullDownRefresh": false }
},
{
"path": "pages/crm/clue/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/clue/form",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/opportunity/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/opportunity/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/customer/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/customer/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/pool/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/quote/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/quote/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/contract/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/crm/contract/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/todo/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/reimburse/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/reimburse/form",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/reimburse/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/advance/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/advance/form",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/advance/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/approve/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/bx/mine/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/db/dashboard/index",
"style": { "navigationStyle": "custom" }
@@ -156,14 +64,6 @@
"path": "pages/db/plans/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/db/weighing/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/db/weighing/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/db/inventory/list",
"style": { "navigationStyle": "custom" }
@@ -173,40 +73,48 @@
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/pl/plan/list",
"path": "pages/db/weighing/list",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/pl/plan/detail",
"path": "pages/db/weighing/detail",
"style": { "navigationStyle": "custom" }
},
{
"path": "pages/pl/plan/arrange",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/advance/list",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "备用金申请" }
},
{
"path": "pages/pl/archive/list",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/reimburse/list",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "报销申请" }
},
{
"path": "pages/pl/cert/list",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/mine/list",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "我的申请单" }
},
{
"path": "pages/pl/cert/detail",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/all/list",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "全部申请单" }
},
{
"path": "pages/pl/freight/list",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/invoice/mine",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "我的发票夹" }
},
{
"path": "pages/pl/freight/detail",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/invoice/admin",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "发票夹管理" }
},
{
"path": "pages/pl/freight/approval",
"style": { "navigationStyle": "custom" }
"path": "pages/bx/admin/expense-item",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "费用项目" }
},
{
"path": "pages/bx/admin/wf-advance",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "工作流(备用金)" }
},
{
"path": "pages/bx/admin/wf-reimburse",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "工作流(报销)" }
}
// #ifdef H5
,
@@ -286,9 +194,6 @@
}
},
"easycom": {
"autoscan": true,
"custom": {
"^van-(.*)": "vant/lib/$1/index.js"
}
"autoscan": true
}
}
@@ -0,0 +1,7 @@
<template>
<bx-placeholder-page default-title="费用项目" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
@@ -0,0 +1,7 @@
<template>
<bx-placeholder-page default-title="工作流(备用金)" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
@@ -0,0 +1,7 @@
<template>
<bx-placeholder-page default-title="工作流(报销)" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
-141
View File
@@ -1,141 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import {
getAdvance,
submitAdvance,
approveAdvance,
rejectAdvance,
getAdvanceApprovalHistory
} from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const id = ref('')
const detail = ref(null)
const history = ref([])
const acting = ref(false)
const isDraft = computed(() => detail.value?.status === 'draft')
const isPending = computed(() => detail.value?.status === 'pending')
const goBack = () => {
uni.navigateBack({
fail: () => uni.navigateTo({ url: PAGE_PATHS.bxAdvanceList })
})
}
const load = async () => {
if (!id.value) return
try {
detail.value = await getAdvance(id.value)
history.value = await getAdvanceApprovalHistory(id.value)
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
}
}
const onSubmit = () => {
uni.showModal({
title: '提交审批',
content: '确认提交该借款单?',
success: async (r) => {
if (!r.confirm) return
acting.value = true
try {
await submitAdvance(id.value)
uni.showToast({ title: '已提交', icon: 'success' })
load()
} catch (e) {
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
} finally {
acting.value = false
}
}
})
}
const doApprove = (pass) => {
uni.showModal({
title: pass ? '审批通过' : '审批驳回',
editable: true,
placeholderText: '审批意见(可选)',
success: async (r) => {
if (!r.confirm) return
acting.value = true
try {
const op = r.content || (pass ? '同意' : '驳回')
if (pass) await approveAdvance(id.value, op)
else await rejectAdvance(id.value, op)
uni.showToast({ title: '操作成功', icon: 'success' })
load()
} catch (e) {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
} finally {
acting.value = false
}
}
})
}
onLoad((q) => {
id.value = q?.id || ''
})
onShow(() => load())
</script>
<template>
<view class="mp-page mp-page--sub mp-page--with-bottom">
<view class="mp-header mp-header--bx">
<text class="mp-subpage-back" @click="goBack">← 返回</text>
<text class="mp-header__title" style="margin-top: 16rpx">借款详情</text>
</view>
<view v-if="detail" class="mp-detail-hero">
<view class="mp-detail-hero__top">
<view>
<text class="mp-detail-hero__title">{{ dash(detail.applyCode) }}</text>
<text class="mp-detail-hero__sub">{{ dash(detail.reason) }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(detail.status).tag">
{{ bxStatusLabel(detail.status).text }}
</text>
</view>
<view class="mp-detail-hero__meta">
<text class="mp-detail-hero__chip">{{ formatMoney(detail.amount) }}</text>
<text class="mp-detail-hero__chip">{{ dash(detail.applicantName) }}</text>
</view>
</view>
<view class="mp-block">
<text class="mp-block__title">基本信息</text>
<view class="mp-block__panel">
<view class="mp-kv"><text class="mp-kv__label">用途</text><text class="mp-kv__value">{{ dash(detail?.purpose) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">申请时间</text><text class="mp-kv__value">{{ dash(detail?.applyTime) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">当前节点</text><text class="mp-kv__value">{{ dash(detail?.currentNode) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">发放状态</text><text class="mp-kv__value">{{ dash(detail?.issueStatus) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">备注</text><text class="mp-kv__value">{{ dash(detail?.remark) }}</text></view>
</view>
</view>
<view v-if="history.length" class="mp-block">
<text class="mp-block__title">审批记录</text>
<view class="mp-block__panel">
<view v-for="h in history" :key="h.id" class="mp-kv">
<text class="mp-kv__label">{{ dash(h.approverName || h.nodeName) }}</text>
<text class="mp-kv__value">{{ dash(h.approvalStatus) }} · {{ dash(h.approvalOpinion) }}</text>
</view>
</view>
</view>
<view class="mp-bottom-bar">
<view v-if="isDraft" class="mp-btn mp-btn--primary" @click="onSubmit">
{{ acting ? '…' : '提交审批' }}
</view>
<template v-if="isPending">
<view class="mp-btn mp-btn--danger" @click="doApprove(false)">驳回</view>
<view class="mp-btn mp-btn--primary" @click="doApprove(true)">通过</view>
</template>
</view>
</view>
</template>
-153
View File
@@ -1,153 +0,0 @@
<script setup>
import { reactive, ref, onMounted } from 'vue'
import {
getExpenseItemTree,
flattenExpenseLeaves,
createAdvance,
submitAdvance,
bindAttachments
} from '@/api/bx'
import { chooseAndUploadBxAttachments } from '@/utils/bxUpload'
import { PAGE_PATHS } from '@/config/api'
const submitting = ref(false)
const leaves = ref([])
const photos = ref([])
const form = reactive({
reason: '',
purpose: '',
amount: '',
remark: '',
expenseItemId: '',
expenseLabel: ''
})
const goBack = () => {
uni.navigateBack({
fail: () => uni.navigateTo({ url: PAGE_PATHS.bxAdvanceList })
})
}
onMounted(async () => {
try {
leaves.value = flattenExpenseLeaves(await getExpenseItemTree())
} catch (e) {
uni.showToast({ title: e.message || '费用项目加载失败', icon: 'none' })
}
})
const pickExpense = () => {
if (!leaves.value.length) {
uni.showToast({ title: '暂无费用项目', icon: 'none' })
return
}
uni.showActionSheet({
itemList: leaves.value.map((i) => i.pathLabel).slice(0, 20),
success: (res) => {
const item = leaves.value[res.tapIndex]
if (!item) return
form.expenseItemId = item.id
form.expenseLabel = item.pathLabel
}
})
}
const addPhotos = async () => {
const uploaded = await chooseAndUploadBxAttachments({
bizType: 'advance_apply',
bizId: '0',
maxCount: 6,
currentCount: photos.value.length
})
photos.value = photos.value.concat(uploaded)
}
const removePhoto = (idx) => photos.value.splice(idx, 1)
const submit = async (andSubmit) => {
if (!String(form.reason).trim()) {
uni.showToast({ title: '请填写借款事由', icon: 'none' })
return
}
if (!form.amount || Number(form.amount) <= 0) {
uni.showToast({ title: '请填写金额', icon: 'none' })
return
}
submitting.value = true
try {
const created = await createAdvance({
reason: String(form.reason).trim(),
purpose: form.purpose || undefined,
amount: Number(form.amount),
remark: form.remark || undefined,
expenseItemId: form.expenseItemId || undefined
})
const id = created?.id
if (id && photos.value.length) {
await bindAttachments(
photos.value.map((p) => p.id),
'advance_apply',
id
)
}
if (andSubmit && id) {
await submitAdvance(id)
uni.showToast({ title: '已提交审批', icon: 'success' })
} else {
uni.showToast({ title: '已保存草稿', icon: 'success' })
}
setTimeout(() => goBack(), 500)
} catch (e) {
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
} finally {
submitting.value = false
}
}
</script>
<template>
<view class="mp-page mp-page--sub mp-page--with-bottom">
<view class="mp-header mp-header--bx">
<text class="mp-subpage-back" @click="goBack">← 返回</text>
<text class="mp-header__title" style="margin-top: 16rpx">新建借款</text>
</view>
<view class="mp-form-panel">
<view class="mp-form-row">
<text class="mp-form-label mp-form-label--req">借款事由</text>
<textarea v-model="form.reason" class="mp-form-textarea" placeholder="用途说明" />
</view>
<view class="mp-form-row">
<text class="mp-form-label">用途</text>
<input v-model="form.purpose" class="mp-form-input" placeholder="可选" />
</view>
<view class="mp-form-row">
<text class="mp-form-label mp-form-label--req">金额(元)</text>
<input v-model="form.amount" class="mp-form-input" type="digit" placeholder="0.00" />
</view>
<view class="mp-form-row" @click="pickExpense">
<text class="mp-form-label">费用项目</text>
<view class="mp-form-input">{{ form.expenseLabel || '可选,点击选择' }}</view>
</view>
<view class="mp-form-row">
<text class="mp-form-label">备注</text>
<textarea v-model="form.remark" class="mp-form-textarea" placeholder="可选" />
</view>
<view class="mp-form-row">
<text class="mp-form-label">凭证照片</text>
<view class="mp-photo-row">
<view v-for="(p, idx) in photos" :key="p.id" class="mp-photo-item">
<image :src="p.localPath" mode="aspectFill" />
<view class="mp-photo-del" @click.stop="removePhoto(idx)">×</view>
</view>
<view v-if="photos.length < 6" class="mp-photo-add" @click="addPhotos">+ 拍照</view>
</view>
</view>
</view>
<view class="mp-bottom-bar">
<view class="mp-btn mp-btn--ghost" @click="submit(false)">{{ submitting ? '…' : '存草稿' }}</view>
<view class="mp-btn mp-btn--primary" @click="submit(true)">{{ submitting ? '…' : '提交审批' }}</view>
</view>
</view>
</template>
+5 -86
View File
@@ -1,88 +1,7 @@
<script setup>
import { ref } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import { listMyAdvance } from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const list = ref([])
const page = ref(1)
const total = ref(0)
const loading = ref(false)
const finished = ref(false)
const pageSize = 20
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
const goForm = () => uni.navigateTo({ url: PAGE_PATHS.bxAdvanceForm })
const goDetail = (row) =>
uni.navigateTo({ url: `${PAGE_PATHS.bxAdvanceDetail}?id=${row.id}` })
const loadList = async (reset = false) => {
if (loading.value) return
if (reset) {
page.value = 1
finished.value = false
list.value = []
}
if (finished.value) return
loading.value = true
try {
const res = await listMyAdvance({ page: page.value, pageSize })
const rows = res.rows || []
list.value = reset ? rows : list.value.concat(rows)
total.value = res.total || 0
if (list.value.length >= total.value || rows.length < pageSize) finished.value = true
else page.value += 1
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
onShow(() => loadList(true))
onReachBottom(() => loadList(false))
</script>
<template>
<view class="mp-page mp-page--sub">
<view class="mp-header mp-header--bx">
<view class="mp-header__row">
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
<view class="mp-header__action" @click="goForm">+ 申请</view>
</view>
<text class="mp-header__title" style="margin-top: 16rpx">借款申请</text>
</view>
<view class="mp-section">
<mp-empty v-if="!list.length && !loading" title="暂无借款单" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
<view>
<text class="mp-card__title">{{ dash(row.applyCode) }}</text>
<text class="mp-card__sub">{{ dash(row.reason) }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(row.status).tag">
{{ bxStatusLabel(row.status).text }}
</text>
</view>
<view class="mp-card__row">
<text>申请时间</text>
<text class="mp-card__row-val">{{ dash(row.applyTime) }}</text>
</view>
<view class="mp-card__foot">
<text class="mp-card__amount">{{ formatMoney(row.amount) }}</text>
</view>
</view>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
<view v-else-if="finished && list.length" class="mp-list-end">已加载全部</view>
</view>
</view>
<bx-placeholder-page default-title="备用金申请" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
+7
View File
@@ -0,0 +1,7 @@
<template>
<bx-placeholder-page default-title="全部申请单" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
-137
View File
@@ -1,137 +0,0 @@
<script setup>
import { ref } from 'vue'
import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app'
import {
listPendingReimburse,
listPendingAdvance,
approveReimburse,
rejectReimburse,
approveAdvance,
rejectAdvance
} from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
/** type: reimburse | advance */
const bizType = ref('reimburse')
const list = ref([])
const page = ref(1)
const total = ref(0)
const loading = ref(false)
const finished = ref(false)
const pageSize = 20
const titleText = ref('报销审批')
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
const goDetail = (row) => {
if (bizType.value === 'advance') {
uni.navigateTo({ url: `${PAGE_PATHS.bxAdvanceDetail}?id=${row.id}` })
} else {
uni.navigateTo({ url: `${PAGE_PATHS.bxReimburseDetail}?id=${row.id}` })
}
}
const loadList = async (reset = false) => {
if (loading.value) return
if (reset) {
page.value = 1
finished.value = false
list.value = []
}
if (finished.value) return
loading.value = true
try {
const q = { page: page.value, pageSize }
const res =
bizType.value === 'advance'
? await listPendingAdvance(q)
: await listPendingReimburse(q)
const rows = res.rows || []
list.value = reset ? rows : list.value.concat(rows)
total.value = res.total || 0
if (list.value.length >= total.value || rows.length < pageSize) finished.value = true
else page.value += 1
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const doApprove = (row, pass) => {
uni.showModal({
title: pass ? '通过' : '驳回',
editable: !pass,
placeholderText: '驳回意见',
success: async (r) => {
if (!r.confirm) return
try {
const op = r.content || (pass ? '同意' : '驳回')
if (bizType.value === 'advance') {
if (pass) await approveAdvance(row.id, op)
else await rejectAdvance(row.id, op)
} else if (pass) await approveReimburse(row.id, op)
else await rejectReimburse(row.id, op)
uni.showToast({ title: '操作成功', icon: 'success' })
loadList(true)
} catch (e) {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
}
})
}
onLoad((q) => {
bizType.value = q?.type === 'advance' ? 'advance' : 'reimburse'
titleText.value = bizType.value === 'advance' ? '借款审批' : '报销审批'
})
onShow(() => loadList(true))
onReachBottom(() => loadList(false))
</script>
<template>
<view class="mp-page mp-page--sub">
<view class="mp-header mp-header--bx">
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
<text class="mp-header__title" style="margin-top: 16rpx">{{ titleText }}</text>
</view>
<view class="mp-section">
<mp-empty v-if="!list.length && !loading" title="暂无待审批" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
<view>
<text class="mp-card__title">{{ dash(row.applyCode) }}</text>
<text class="mp-card__sub">
{{ bizType === 'advance' ? dash(row.reason) : dash(row.expenseName || row.title) }}
</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(row.status).tag">
{{ bxStatusLabel(row.status).text }}
</text>
</view>
<view class="mp-card__row">
<text>申请人</text>
<text class="mp-card__row-val">{{ dash(row.applicantName) }}</text>
</view>
<view class="mp-card__foot">
<text class="mp-card__amount">{{ formatMoney(row.amount) }}</text>
<view style="display: flex; gap: 16rpx" @click.stop>
<view class="mp-btn mp-btn--danger mp-btn--sm" @click="doApprove(row, false)">驳回</view>
<view class="mp-btn mp-btn--primary mp-btn--sm" @click="doApprove(row, true)">通过</view>
</view>
</view>
</view>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
<view v-else-if="finished && list.length" class="mp-list-end">已加载全部</view>
</view>
</view>
</template>
+7
View File
@@ -0,0 +1,7 @@
<template>
<bx-placeholder-page default-title="发票夹管理" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
+7
View File
@@ -0,0 +1,7 @@
<template>
<bx-placeholder-page default-title="我的发票夹" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
+5 -139
View File
@@ -1,141 +1,7 @@
<script setup>
import { ref } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import { listMyReimburse, listMyAdvance } from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const tabs = [
{ key: 'all', label: '全部' },
{ key: 'reimburse', label: '报销' },
{ key: 'advance', label: '借款' }
]
const activeTab = ref('all')
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const page = ref(1)
const pageSize = 20
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
const goDetail = (row) => {
if (row._biz === 'advance') {
uni.navigateTo({ url: `${PAGE_PATHS.bxAdvanceDetail}?id=${row.id}` })
} else {
uni.navigateTo({ url: `${PAGE_PATHS.bxReimburseDetail}?id=${row.id}` })
}
}
const mark = (rows, biz) =>
(rows || []).map((r) => ({
...r,
_biz: biz,
_title: biz === 'advance' ? `借款 ${r.applyCode || ''}` : `报销 ${r.applyCode || ''}`,
_sub: biz === 'advance' ? dash(r.reason) : dash(r.expenseName || r.title)
}))
const loadList = async (reset = false) => {
if (loading.value) return
if (reset) {
page.value = 1
finished.value = false
list.value = []
}
if (finished.value) return
loading.value = true
try {
const q = { page: page.value, pageSize }
let rows = []
if (activeTab.value === 'reimburse') {
const res = await listMyReimburse(q)
rows = mark(res.rows, 'reimburse')
list.value = reset ? rows : list.value.concat(rows)
if (list.value.length >= (res.total || 0) || rows.length < pageSize) finished.value = true
else page.value += 1
} else if (activeTab.value === 'advance') {
const res = await listMyAdvance(q)
rows = mark(res.rows, 'advance')
list.value = reset ? rows : list.value.concat(rows)
if (list.value.length >= (res.total || 0) || rows.length < pageSize) finished.value = true
else page.value += 1
} else {
const [a, b] = await Promise.all([listMyReimburse(q), listMyAdvance(q)])
rows = mark(a.rows, 'reimburse').concat(mark(b.rows, 'advance'))
rows.sort((x, y) => String(y.applyTime || '').localeCompare(String(x.applyTime || '')))
list.value = reset ? rows : list.value.concat(rows)
const total = (a.total || 0) + (b.total || 0)
if (list.value.length >= total || (a.rows.length < pageSize && b.rows.length < pageSize)) {
finished.value = true
} else page.value += 1
}
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const switchTab = (key) => {
if (activeTab.value === key) return
activeTab.value = key
loadList(true)
}
onShow(() => loadList(true))
onReachBottom(() => loadList(false))
</script>
<template>
<view class="mp-page mp-page--sub">
<view class="mp-header mp-header--bx">
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
<text class="mp-header__title" style="margin-top: 16rpx">我的单据</text>
</view>
<view class="mp-section" style="padding-bottom: 0">
<scroll-view scroll-x class="mp-tabs">
<view
v-for="t in tabs"
:key="t.key"
class="mp-tab"
:class="{ 'mp-tab--active': activeTab === t.key }"
@click="switchTab(t.key)"
>
{{ t.label }}
</view>
</scroll-view>
</view>
<view class="mp-section">
<mp-empty v-if="!list.length && !loading" title="暂无单据" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row._biz + '-' + row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
<view>
<text class="mp-card__title">{{ row._title }}</text>
<text class="mp-card__sub">{{ row._sub }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(row.status).tag">
{{ bxStatusLabel(row.status).text }}
</text>
</view>
<view class="mp-card__row">
<text>申请时间</text>
<text class="mp-card__row-val">{{ dash(row.applyTime) }}</text>
</view>
<view class="mp-card__foot">
<text class="mp-card__amount">{{ formatMoney(row.amount) }}</text>
</view>
</view>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
<view v-else-if="finished && list.length" class="mp-list-end">已加载全部</view>
</view>
</view>
<bx-placeholder-page default-title="我的申请单" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
-181
View File
@@ -1,181 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import {
getReimburse,
submitReimburse,
approveReimburse,
rejectReimburse,
getReimburseApprovalHistory,
listAttachments,
previewAttachment
} from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const id = ref('')
const detail = ref(null)
const history = ref([])
const attachUrls = ref([])
const acting = ref(false)
const isDraft = computed(() => detail.value?.status === 'draft')
const isPending = computed(() => detail.value?.status === 'pending')
const goBack = () => {
uni.navigateBack({
fail: () => uni.navigateTo({ url: PAGE_PATHS.bxReimburseList })
})
}
const load = async () => {
if (!id.value) return
try {
detail.value = await getReimburse(id.value)
history.value = await getReimburseApprovalHistory(id.value)
const files = await listAttachments('reimburse_apply', id.value)
const urls = []
for (const f of files) {
try {
const prev = await previewAttachment(f.id)
if (prev?.url) urls.push(prev.url)
} catch {
/* skip */
}
}
attachUrls.value = urls
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
}
}
const onSubmit = () => {
uni.showModal({
title: '提交审批',
content: '确认提交该报销单?',
success: async (r) => {
if (!r.confirm) return
acting.value = true
try {
await submitReimburse(id.value)
uni.showToast({ title: '已提交', icon: 'success' })
load()
} catch (e) {
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
} finally {
acting.value = false
}
}
})
}
const doApprove = (pass) => {
uni.showModal({
title: pass ? '审批通过' : '审批驳回',
editable: true,
placeholderText: '审批意见(可选)',
success: async (r) => {
if (!r.confirm) return
acting.value = true
try {
const op = r.content || (pass ? '同意' : '驳回')
if (pass) await approveReimburse(id.value, op)
else await rejectReimburse(id.value, op)
uni.showToast({ title: '操作成功', icon: 'success' })
load()
} catch (e) {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
} finally {
acting.value = false
}
}
})
}
const preview = (url) => {
uni.previewImage({ urls: attachUrls.value, current: url })
}
onLoad((q) => {
id.value = q?.id || ''
})
onShow(() => load())
</script>
<template>
<view class="mp-page mp-page--sub mp-page--with-bottom">
<view class="mp-header mp-header--bx">
<text class="mp-subpage-back" @click="goBack">← 返回</text>
<text class="mp-header__title" style="margin-top: 16rpx">报销详情</text>
</view>
<view v-if="detail" class="mp-detail-hero">
<view class="mp-detail-hero__top">
<view>
<text class="mp-detail-hero__title">{{ dash(detail.applyCode) }}</text>
<text class="mp-detail-hero__sub">{{ dash(detail.title) }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(detail.status).tag">
{{ bxStatusLabel(detail.status).text }}
</text>
</view>
<view class="mp-detail-hero__meta">
<text class="mp-detail-hero__chip">{{ formatMoney(detail.amount) }}</text>
<text class="mp-detail-hero__chip">{{ dash(detail.applicantName) }}</text>
</view>
</view>
<view class="mp-block">
<text class="mp-block__title">基本信息</text>
<view class="mp-block__panel">
<view class="mp-kv"><text class="mp-kv__label">费用项目</text><text class="mp-kv__value">{{ dash(detail?.expenseName) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">报销日期</text><text class="mp-kv__value">{{ dash(detail?.reimburseDate) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">申请时间</text><text class="mp-kv__value">{{ dash(detail?.applyTime) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">当前节点</text><text class="mp-kv__value">{{ dash(detail?.currentNode) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">备注</text><text class="mp-kv__value">{{ dash(detail?.remark) }}</text></view>
</view>
</view>
<view v-if="detail?.details?.length" class="mp-block">
<text class="mp-block__title">明细</text>
<view class="mp-list">
<view v-for="d in detail.details" :key="d.id" class="mp-card">
<view class="mp-card__head">
<text class="mp-card__title">{{ dash(d.expenseName) }}</text>
<text class="mp-card__amount">{{ formatMoney(d.amount) }}</text>
</view>
<text style="font-size: 24rpx; color: var(--mp-muted)">{{ dash(d.remark) }}</text>
</view>
</view>
</view>
<view v-if="attachUrls.length" class="mp-block">
<text class="mp-block__title">票据</text>
<view class="mp-photo-row" style="padding: 0 0 8rpx">
<view v-for="(url, i) in attachUrls" :key="i" class="mp-photo-item" @click="preview(url)">
<image :src="url" mode="aspectFill" />
</view>
</view>
</view>
<view v-if="history.length" class="mp-block">
<text class="mp-block__title">审批记录</text>
<view class="mp-block__panel">
<view v-for="h in history" :key="h.id" class="mp-kv">
<text class="mp-kv__label">{{ dash(h.approverName || h.nodeName) }}</text>
<text class="mp-kv__value">{{ dash(h.approvalStatus) }} · {{ dash(h.approvalOpinion) }}</text>
</view>
</view>
</view>
<view class="mp-bottom-bar">
<view v-if="isDraft" class="mp-btn mp-btn--primary" @click="onSubmit">
{{ acting ? '…' : '提交审批' }}
</view>
<template v-if="isPending">
<view class="mp-btn mp-btn--danger" @click="doApprove(false)">驳回</view>
<view class="mp-btn mp-btn--primary" @click="doApprove(true)">通过</view>
</template>
</view>
</view>
</template>
-190
View File
@@ -1,190 +0,0 @@
<script setup>
import { reactive, ref, onMounted } from 'vue'
import {
getExpenseItemTree,
flattenExpenseLeaves,
createReimburse,
submitReimburse,
generateReimburseTitle,
bindAttachments
} from '@/api/bx'
import { chooseAndUploadBxAttachments } from '@/utils/bxUpload'
import { todayStr } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const submitting = ref(false)
const leaves = ref([])
const photos = ref([])
const form = reactive({
title: '',
reimburseDate: todayStr(),
remark: '',
expenseItemId: '',
expenseLabel: '',
amount: '',
detailRemark: ''
})
const goBack = () => {
uni.navigateBack({
fail: () => uni.navigateTo({ url: PAGE_PATHS.bxReimburseList })
})
}
const loadTree = async () => {
try {
const tree = await getExpenseItemTree()
leaves.value = flattenExpenseLeaves(tree)
} catch (e) {
uni.showToast({ title: e.message || '费用项目加载失败', icon: 'none' })
}
}
const refreshTitle = async () => {
try {
const t = await generateReimburseTitle(form.reimburseDate)
if (typeof t === 'string') form.title = t
else if (t) form.title = String(t)
} catch {
/* optional */
}
}
const pickExpense = () => {
if (!leaves.value.length) {
uni.showToast({ title: '暂无费用项目,请先在 Web 配置', icon: 'none' })
return
}
const names = leaves.value.map((i) => i.pathLabel).slice(0, 20)
uni.showActionSheet({
itemList: names,
success: (res) => {
const item = leaves.value[res.tapIndex]
if (!item) return
form.expenseItemId = item.id
form.expenseLabel = item.pathLabel
}
})
}
const addPhotos = async () => {
const uploaded = await chooseAndUploadBxAttachments({
bizType: 'reimburse_apply',
bizId: '0',
maxCount: 6,
currentCount: photos.value.length
})
photos.value = photos.value.concat(uploaded)
}
const removePhoto = (idx) => {
photos.value.splice(idx, 1)
}
const submit = async (andSubmit) => {
if (!form.expenseItemId) {
uni.showToast({ title: '请选择费用项目', icon: 'none' })
return
}
if (!form.amount || Number(form.amount) <= 0) {
uni.showToast({ title: '请填写金额', icon: 'none' })
return
}
if (!String(form.detailRemark || '').trim()) {
uni.showToast({ title: '请填写明细说明', icon: 'none' })
return
}
submitting.value = true
try {
const created = await createReimburse({
title: form.title || undefined,
reimburseDate: form.reimburseDate,
remark: form.remark,
details: [
{
expenseItemId: form.expenseItemId,
amount: Number(form.amount),
remark: String(form.detailRemark).trim()
}
]
})
const id = created?.id
if (id && photos.value.length) {
await bindAttachments(
photos.value.map((p) => p.id),
'reimburse_apply',
id
)
}
if (andSubmit && id) {
await submitReimburse(id)
uni.showToast({ title: '已提交审批', icon: 'success' })
} else {
uni.showToast({ title: '已保存草稿', icon: 'success' })
}
setTimeout(() => goBack(), 500)
} catch (e) {
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
} finally {
submitting.value = false
}
}
onMounted(async () => {
await loadTree()
await refreshTitle()
})
</script>
<template>
<view class="mp-page mp-page--sub mp-page--with-bottom">
<view class="mp-header mp-header--bx">
<text class="mp-subpage-back" @click="goBack">← 返回</text>
<text class="mp-header__title" style="margin-top: 16rpx">新建报销</text>
</view>
<view class="mp-form-panel">
<view class="mp-form-row">
<text class="mp-form-label">标题</text>
<input v-model="form.title" class="mp-form-input" placeholder="自动生成,可修改" />
</view>
<view class="mp-form-row">
<text class="mp-form-label">报销日期</text>
<picker mode="date" :value="form.reimburseDate" @change="(e) => { form.reimburseDate = e.detail.value; refreshTitle() }">
<view class="mp-form-input">{{ form.reimburseDate }}</view>
</picker>
</view>
<view class="mp-form-row" @click="pickExpense">
<text class="mp-form-label mp-form-label--req">费用项目</text>
<view class="mp-form-input">{{ form.expenseLabel || '点击选择' }}</view>
</view>
<view class="mp-form-row">
<text class="mp-form-label mp-form-label--req">金额(元)</text>
<input v-model="form.amount" class="mp-form-input" type="digit" placeholder="0.00" />
</view>
<view class="mp-form-row">
<text class="mp-form-label mp-form-label--req">明细说明</text>
<textarea v-model="form.detailRemark" class="mp-form-textarea" placeholder="费用事由" />
</view>
<view class="mp-form-row">
<text class="mp-form-label">备注</text>
<textarea v-model="form.remark" class="mp-form-textarea" placeholder="可选" />
</view>
<view class="mp-form-row">
<text class="mp-form-label">票据照片</text>
<view class="mp-photo-row">
<view v-for="(p, idx) in photos" :key="p.id" class="mp-photo-item">
<image :src="p.localPath" mode="aspectFill" />
<view class="mp-photo-del" @click.stop="removePhoto(idx)">×</view>
</view>
<view v-if="photos.length < 6" class="mp-photo-add" @click="addPhotos">+ 拍照</view>
</view>
</view>
</view>
<view class="mp-bottom-bar">
<view class="mp-btn mp-btn--ghost" @click="submit(false)">{{ submitting ? '…' : '存草稿' }}</view>
<view class="mp-btn mp-btn--primary" @click="submit(true)">{{ submitting ? '…' : '提交审批' }}</view>
</view>
</view>
</template>
+5 -86
View File
@@ -1,88 +1,7 @@
<script setup>
import { ref } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import { listMyReimburse } from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const list = ref([])
const page = ref(1)
const total = ref(0)
const loading = ref(false)
const finished = ref(false)
const pageSize = 20
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
const goForm = () => uni.navigateTo({ url: PAGE_PATHS.bxReimburseForm })
const goDetail = (row) =>
uni.navigateTo({ url: `${PAGE_PATHS.bxReimburseDetail}?id=${row.id}` })
const loadList = async (reset = false) => {
if (loading.value) return
if (reset) {
page.value = 1
finished.value = false
list.value = []
}
if (finished.value) return
loading.value = true
try {
const res = await listMyReimburse({ page: page.value, pageSize })
const rows = res.rows || []
list.value = reset ? rows : list.value.concat(rows)
total.value = res.total || 0
if (list.value.length >= total.value || rows.length < pageSize) finished.value = true
else page.value += 1
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
onShow(() => loadList(true))
onReachBottom(() => loadList(false))
</script>
<template>
<view class="mp-page mp-page--sub">
<view class="mp-header mp-header--bx">
<view class="mp-header__row">
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
<view class="mp-header__action" @click="goForm">+ 申请</view>
</view>
<text class="mp-header__title" style="margin-top: 16rpx">报销申请</text>
</view>
<view class="mp-section">
<mp-empty v-if="!list.length && !loading" title="暂无报销单" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
<view>
<text class="mp-card__title">{{ dash(row.applyCode) }}</text>
<text class="mp-card__sub">{{ dash(row.expenseName || row.title) }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(row.status).tag">
{{ bxStatusLabel(row.status).text }}
</text>
</view>
<view class="mp-card__row">
<text>申请时间</text>
<text class="mp-card__row-val">{{ dash(row.applyTime) }}</text>
</view>
<view class="mp-card__foot">
<text class="mp-card__amount">{{ formatMoney(row.amount) }}</text>
</view>
</view>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
<view v-else-if="finished && list.length" class="mp-list-end">已加载全部</view>
</view>
</view>
<bx-placeholder-page default-title="报销申请" />
</template>
<script setup>
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
</script>
-207
View File
@@ -1,207 +0,0 @@
<script setup>
import { ref } from 'vue'
import { onShow, onReachBottom } from '@dcloudio/uni-app'
import {
listPendingReimburse,
listPendingAdvance,
listMyReimburse,
listMyAdvance,
listApprovedByMeReimburse,
listApprovedByMeAdvance,
approveReimburse,
rejectReimburse,
approveAdvance,
rejectAdvance
} from '@/api/bx'
import { bxStatusLabel, formatMoney, dash } from '@/utils/bxLabels'
import { PAGE_PATHS } from '@/config/api'
const tabs = [
{ key: 'pending', label: '待审批' },
{ key: 'mine', label: '我的申请' },
{ key: 'done', label: '已办' }
]
const activeTab = ref('pending')
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const page = ref(1)
const pageSize = 10
const goBack = () => {
uni.navigateBack({
fail: () => uni.switchTab({ url: PAGE_PATHS.workbench })
})
}
const goApply = () => {
uni.showActionSheet({
itemList: ['报销申请', '借款申请'],
success: (res) => {
if (res.tapIndex === 0) uni.navigateTo({ url: PAGE_PATHS.bxReimburseForm })
if (res.tapIndex === 1) uni.navigateTo({ url: PAGE_PATHS.bxAdvanceForm })
}
})
}
const goDetail = (row) => {
if (row._biz === 'advance') {
uni.navigateTo({ url: `${PAGE_PATHS.bxAdvanceDetail}?id=${row.id}` })
} else {
uni.navigateTo({ url: `${PAGE_PATHS.bxReimburseDetail}?id=${row.id}` })
}
}
const markBiz = (rows, biz) =>
(rows || []).map((r) => ({
...r,
_biz: biz,
_title: biz === 'advance' ? `借款单 ${r.applyCode || ''}` : `报销单 ${r.applyCode || ''}`,
_sub:
biz === 'advance'
? dash(r.reason || r.purpose)
: dash(r.expenseName || r.title)
}))
const loadList = async (reset = false) => {
if (loading.value) return
if (reset) {
page.value = 1
finished.value = false
list.value = []
}
if (finished.value) return
loading.value = true
try {
const q = { page: page.value, pageSize }
let rows = []
if (activeTab.value === 'pending') {
const [a, b] = await Promise.all([listPendingReimburse(q), listPendingAdvance(q)])
rows = markBiz(a.rows, 'reimburse').concat(markBiz(b.rows, 'advance'))
rows.sort((x, y) => String(y.applyTime || '').localeCompare(String(x.applyTime || '')))
finished.value = true
} else if (activeTab.value === 'mine') {
const [a, b] = await Promise.all([listMyReimburse(q), listMyAdvance(q)])
rows = markBiz(a.rows, 'reimburse').concat(markBiz(b.rows, 'advance'))
rows.sort((x, y) => String(y.applyTime || '').localeCompare(String(x.applyTime || '')))
const total = (a.total || 0) + (b.total || 0)
list.value = reset ? rows : list.value.concat(rows)
if (list.value.length >= total || (a.rows.length < pageSize && b.rows.length < pageSize)) {
finished.value = true
} else page.value += 1
return
} else {
const [a, b] = await Promise.all([
listApprovedByMeReimburse(q),
listApprovedByMeAdvance(q)
])
rows = markBiz(a.rows, 'reimburse').concat(markBiz(b.rows, 'advance'))
rows.sort((x, y) => String(y.applyTime || '').localeCompare(String(x.applyTime || '')))
finished.value = true
}
list.value = reset ? rows : list.value.concat(rows)
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const switchTab = (key) => {
if (activeTab.value === key) return
activeTab.value = key
loadList(true)
}
const doApprove = (row, pass) => {
const title = pass ? '通过' : '驳回'
uni.showModal({
title: `确认${title}`,
editable: !pass,
placeholderText: pass ? '' : '驳回意见(可选)',
success: async (r) => {
if (!r.confirm) return
try {
const opinion = r.content || (pass ? '同意' : '驳回')
if (row._biz === 'advance') {
if (pass) await approveAdvance(row.id, opinion)
else await rejectAdvance(row.id, opinion)
} else {
if (pass) await approveReimburse(row.id, opinion)
else await rejectReimburse(row.id, opinion)
}
uni.showToast({ title: `${title}成功`, icon: 'success' })
loadList(true)
} catch (e) {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
}
})
}
onShow(() => loadList(true))
onReachBottom(() => {
if (activeTab.value === 'mine') loadList(false)
})
</script>
<template>
<view class="mp-page mp-page--sub">
<view class="mp-header mp-header--bx">
<view class="mp-header__row">
<text class="mp-subpage-back" @click="goBack">← 工作台</text>
<view class="mp-header__action" @click="goApply">+ 申请</view>
</view>
<text class="mp-header__title" style="margin-top: 16rpx">待办任务</text>
</view>
<view class="mp-section" style="padding-bottom: 0">
<scroll-view scroll-x class="mp-tabs">
<view
v-for="t in tabs"
:key="t.key"
class="mp-tab"
:class="{ 'mp-tab--active': activeTab === t.key }"
@click="switchTab(t.key)"
>
{{ t.label }}
</view>
</scroll-view>
</view>
<view class="mp-section">
<mp-empty v-if="!list.length && !loading" title="暂无单据" />
<view v-else class="mp-list">
<view v-for="row in list" :key="row._biz + '-' + row.id" class="mp-card" @click="goDetail(row)">
<view class="mp-card__head">
<view>
<text class="mp-card__title">{{ row._title }}</text>
<text class="mp-card__sub">{{ row._sub }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + bxStatusLabel(row.status).tag">
{{ bxStatusLabel(row.status).text }}
</text>
</view>
<view class="mp-card__row">
<text>申请人</text>
<text class="mp-card__row-val">{{ dash(row.applicantName) }}</text>
</view>
<view class="mp-card__row">
<text>申请时间</text>
<text class="mp-card__row-val">{{ dash(row.applyTime) }}</text>
</view>
<view class="mp-card__foot">
<text class="mp-card__amount">{{ formatMoney(row.amount) }}</text>
<view v-if="activeTab === 'pending'" style="display: flex; gap: 16rpx" @click.stop>
<view class="mp-btn mp-btn--danger mp-btn--sm" @click="doApprove(row, false)">驳回</view>
<view class="mp-btn mp-btn--primary mp-btn--sm" @click="doApprove(row, true)">通过</view>
</view>
</view>
</view>
</view>
<view v-if="loading" class="mp-list-loading">加载中…</view>
<view v-else-if="finished && list.length" class="mp-list-end">已加载全部</view>
</view>
</view>
</template>