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:
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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<bx-placeholder-page default-title="全部申请单" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BxPlaceholderPage from '@/components/bx-ui/bx-placeholder-page.vue'
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user