Commit 28dc12db7291ec5d08981eec693d807ee073993f

Authored by 李宇
1 parent 07c554d7

```

feat(reimbursement): 实现报销申请编辑功能并优化详情展示

- 新增 `edit-dialog.vue` 组件,支持修改报销申请信息
- 在详情页中展示购买记录列表及其附件预览
- 优化审批按钮权限判断逻辑,确保仅当前审批人可操作
- 调整接口调用方式,适配后端API变更
- 注释部分旧布局代码,为后续调整做准备
- 样式微调,提升用户体验与界面一致性
```
antis-ncc-admin/src/views/lqReimbursementApplication/approval-history-dialog.vue
... ... @@ -162,7 +162,7 @@ export default {
162 162  
163 163 <style lang="scss" scoped>
164 164 .history-content {
165   - max-height: 70vh;
  165 + //max-height: 70vh;
166 166 overflow-y: auto;
167 167 padding: 20px 0;
168 168 }
... ...
antis-ncc-admin/src/views/lqReimbursementApplication/detail-dialog.vue
... ... @@ -15,7 +15,7 @@
15 15 <span class="card-title">申请信息</span>
16 16 </div>
17 17 <el-row :gutter="20">
18   - <el-col :span="8">
  18 + <!-- <el-col :span="8">
19 19 <div class="info-item">
20 20 <label class="info-label">申请编号:</label>
21 21 <span class="info-value">{{ formData.id || '无' }}</span>
... ... @@ -26,7 +26,7 @@
26 26 <label class="info-label">申请人编号:</label>
27 27 <span class="info-value">{{ formData.applicationUserId || '无' }}</span>
28 28 </div>
29   - </el-col>
  29 + </el-col> -->
30 30 <el-col :span="8">
31 31 <div class="info-item">
32 32 <label class="info-label">申请人姓名:</label>
... ... @@ -35,8 +35,8 @@
35 35 </el-col>
36 36 <el-col :span="8">
37 37 <div class="info-item">
38   - <label class="info-label">申请门店ID:</label>
39   - <span class="info-value">{{ formData.applicationStoreId || '无' }}</span>
  38 + <label class="info-label">申请门店:</label>
  39 + <span class="info-value">{{ formData.applicationStoreName || '无' }}</span>
40 40 </div>
41 41 </el-col>
42 42 <el-col :span="8">
... ... @@ -51,7 +51,7 @@
51 51 <span class="info-value amount">{{ formatMoney(formData.amount) || '无' }}</span>
52 52 </div>
53 53 </el-col>
54   - <el-col :span="8">
  54 + <!-- <el-col :span="8">
55 55 <div class="info-item">
56 56 <label class="info-label">审批状态:</label>
57 57 <el-tag
... ... @@ -67,10 +67,80 @@
67 67 <label class="info-label">购买记录编号:</label>
68 68 <span class="info-value">{{ formData.purchaseRecordsId || '无' }}</span>
69 69 </div>
70   - </el-col>
  70 + </el-col> -->
71 71 </el-row>
72 72 </el-card>
73 73  
  74 + <!-- 购买记录列表 -->
  75 + <el-card class="info-card" shadow="hover" v-if="purchaseRecords && purchaseRecords.length > 0">
  76 + <div slot="header" class="card-header">
  77 + <i class="el-icon-shopping-cart-full"></i>
  78 + <span class="card-title">购买记录列表</span>
  79 + </div>
  80 + <el-table :data="purchaseRecords" border size="small" style="width: 100%">
  81 + <el-table-column prop="reimbursementCategoryName" label="分类名称" >
  82 + <template slot-scope="scope">
  83 + <div class="category-item">
  84 + <span>{{ scope.row.reimbursementCategoryName || '无' }}</span>
  85 + </div>
  86 + </template>
  87 + </el-table-column>
  88 + <el-table-column prop="unitPrice" label="单价" >
  89 + <template slot-scope="scope">
  90 + <div class="price-item">
  91 + <span>{{ formatMoney(scope.row.unitPrice) }}</span>
  92 + </div>
  93 + </template>
  94 + </el-table-column>
  95 + <el-table-column prop="quantity" label="数量" >
  96 + <template slot-scope="scope">
  97 + <div class="quantity-item">
  98 + <span>{{ scope.row.quantity || '无' }}</span>
  99 + </div>
  100 + </template>
  101 + </el-table-column>
  102 + <el-table-column prop="amount" label="金额" >
  103 + <template slot-scope="scope">
  104 + <div class="amount-item">
  105 + <span class="amount-value">{{ formatMoney(scope.row.amount) }}</span>
  106 + </div>
  107 + </template>
  108 + </el-table-column>
  109 + <el-table-column prop="purchaseTime" label="购买时间" width="180" >
  110 + <template slot-scope="scope">
  111 + <div class="time-item">
  112 + <span>{{ formatDateTime(scope.row.purchaseTime) || '无' }}</span>
  113 + </div>
  114 + </template>
  115 + </el-table-column>
  116 + <el-table-column prop="memo" label="备注说明" show-overflow-tooltip>
  117 + <template slot-scope="scope">
  118 + {{ scope.row.memo || '无' }}
  119 + </template>
  120 + </el-table-column>
  121 + <el-table-column prop="attachment" label="附件" width="100" >
  122 + <template slot-scope="scope">
  123 + <div v-if="scope.row.attachment && scope.row.attachment.length > 0" class="attachment-list">
  124 + <div>
  125 + <i class="el-icon-paperclip"></i>
  126 + <span class="attachment-name">{{ scope.row.attachment.length + '个文件' }}</span>
  127 + </div>
  128 + <div class="attachment-item">
  129 + <el-image :src="baseUrl + scope.row.attachment[0].url" fit="contain" :style="{
  130 + width: '100%',
  131 + height: '100px',
  132 + display: 'block',
  133 + margin: '0 auto'
  134 + }" :preview-src-list="getPreviewSrcList(scope.row.attachment)">
  135 + </el-image>
  136 + </div>
  137 + </div>
  138 + <span v-else>无</span>
  139 + </template>
  140 + </el-table-column>
  141 + </el-table>
  142 + </el-card>
  143 +
74 144 <!-- 审批流程卡片 -->
75 145 <el-card class="info-card" shadow="hover" v-if="nodes && nodes.length > 0">
76 146 <div slot="header" class="card-header">
... ... @@ -223,6 +293,7 @@ export default {
223 293 name: 'DetailDialog',
224 294 data() {
225 295 return {
  296 + baseUrl: process.env.VUE_APP_BASE_API,
226 297 visible: false,
227 298 loading: false,
228 299 approving: false,
... ... @@ -232,6 +303,7 @@ export default {
232 303 currentNodeOrder: 0,
233 304 approvalStatus: '',
234 305 returnedReason: null,
  306 + purchaseRecords: [], // 购买记录列表数据
235 307 approvalForm: {
236 308 result: '通过',
237 309 opinion: ''
... ... @@ -244,12 +316,31 @@ export default {
244 316 if (this.approvalStatus !== '审批中') {
245 317 return false
246 318 }
247   - // 这里可以根据实际登录用户信息判断
248   - // 暂时返回true,实际应该判断当前登录用户是否在currentApprovers中
249   - return true
  319 +
  320 + // 获取当前登录用户信息
  321 + const currentUser = this.$store.getters.userInfo
  322 + if (!currentUser || !currentUser.userId) {
  323 + return false
  324 + }
  325 +
  326 + // 判断当前用户是否在currentApprovers中
  327 + if (!this.currentApprovers || this.currentApprovers.length === 0) {
  328 + return false
  329 + }
  330 +
  331 + // 检查当前用户的userId是否在审批人列表中
  332 + const isCurrentApprover = this.currentApprovers.some(approver => {
  333 + return approver.userId === currentUser.userId
  334 + })
  335 +
  336 + return isCurrentApprover
250 337 }
251 338 },
252 339 methods: {
  340 + // 获取预览图片列表
  341 + getPreviewSrcList(attachment) {
  342 + return attachment.map(item => this.baseUrl + item.url)
  343 + },
253 344 init(id) {
254 345 this.visible = true
255 346 this.loading = true
... ... @@ -259,6 +350,7 @@ export default {
259 350 this.currentNodeOrder = 0
260 351 this.approvalStatus = ''
261 352 this.returnedReason = null
  353 + this.purchaseRecords = []
262 354 this.approvalForm = {
263 355 result: '通过',
264 356 opinion: ''
... ... @@ -280,6 +372,11 @@ export default {
280 372 this.currentNodeOrder = response.data.currentNodeOrder || 0
281 373 this.approvalStatus = response.data.approvalStatus || ''
282 374 this.returnedReason = response.data.returnedReason || null
  375 + this.purchaseRecords = response.data.purchaseRecords || []
  376 + this.purchaseRecords = this.purchaseRecords.map(item => {
  377 + item.attachment = item.attachment?JSON.parse(item.attachment):[]
  378 + return item
  379 + })
283 380 } else {
284 381 this.$message.error(response.msg || '获取详情失败')
285 382 }
... ... @@ -300,12 +397,8 @@ export default {
300 397 this.approving = true
301 398 try {
302 399 const response = await request({
303   - url: `/api/Extend/LqReimbursementApplication/${this.formData.id}/Actions/Approve`,
304   - method: 'POST',
305   - data: {
306   - result: this.approvalForm.result,
307   - opinion: this.approvalForm.opinion || ''
308   - }
  400 + url: `/api/Extend/LqReimbursementApplication/${this.formData.id}/Actions/Approve?result=${this.approvalForm.result}&opinion=${this.approvalForm.opinion || ''}`,
  401 + method: 'POST'
309 402 })
310 403  
311 404 if (response.code === 200) {
... ... @@ -331,6 +424,7 @@ export default {
331 424 this.currentNodeOrder = 0
332 425 this.approvalStatus = ''
333 426 this.returnedReason = null
  427 + this.purchaseRecords = []
334 428 this.approvalForm = {
335 429 result: '通过',
336 430 opinion: ''
... ... @@ -396,7 +490,7 @@ export default {
396 490  
397 491 <style lang="scss" scoped>
398 492 .detail-content {
399   - max-height: 70vh;
  493 + //max-height: 70vh;
400 494 overflow-y: auto;
401 495 }
402 496  
... ... @@ -575,5 +669,51 @@ export default {
575 669 color: #F56C6C;
576 670 line-height: 1.6;
577 671 }
  672 +
  673 +// 购买记录表格项样式
  674 +.category-item,
  675 +.price-item,
  676 +.quantity-item,
  677 +.amount-item,
  678 +.time-item {
  679 + display: flex;
  680 + align-items: center;
  681 + justify-content: center;
  682 + gap: 6px;
  683 +
  684 + .category-icon {
  685 + color: #67C23A;
  686 + font-size: 16px;
  687 + }
  688 +
  689 + .price-icon {
  690 + color: #909399;
  691 + font-size: 16px;
  692 + }
  693 +
  694 + .quantity-icon {
  695 + color: #409EFF;
  696 + font-size: 16px;
  697 + }
  698 +
  699 + .amount-icon {
  700 + color: #F56C6C;
  701 + font-size: 16px;
  702 + }
  703 +
  704 + .time-icon {
  705 + color: #909399;
  706 + font-size: 16px;
  707 + }
  708 +
  709 + span {
  710 + color: #606266;
  711 + }
  712 +}
  713 +
  714 +.amount-value {
  715 + font-weight: 600;
  716 + color: #F56C6C;
  717 +}
578 718 </style>
579 719  
... ...
antis-ncc-admin/src/views/lqReimbursementApplication/edit-dialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="修改报销申请"
  4 + :visible.sync="visible"
  5 + :close-on-click-modal="false"
  6 + class="NCC-dialog NCC-dialog_center"
  7 + width="1200px"
  8 + @close="handleClose"
  9 + >
  10 + <div class="form-content" v-loading="loading">
  11 + <el-form
  12 + ref="form"
  13 + :model="dataForm"
  14 + :rules="rules"
  15 + label-width="120px"
  16 + label-position="right"
  17 + >
  18 + <!-- 基本信息 -->
  19 + <el-card class="form-card" shadow="hover">
  20 + <div slot="header" class="card-header">
  21 + <i class="el-icon-document"></i>
  22 + <span class="card-title">基本信息</span>
  23 + </div>
  24 + <el-row :gutter="20">
  25 + <el-col :span="12">
  26 + <el-form-item label="申请人" prop="applicationUserId">
  27 + <user-select
  28 + v-model="dataForm.applicationUserId"
  29 + placeholder="请选择申请人"
  30 + clearable
  31 + @change="handleUserChange"
  32 + />
  33 + </el-form-item>
  34 + </el-col>
  35 + <el-col :span="12">
  36 + <el-form-item label="申请人姓名" prop="applicationUserName">
  37 + <el-input
  38 + v-model="dataForm.applicationUserName"
  39 + placeholder="自动填充"
  40 + readonly
  41 + />
  42 + </el-form-item>
  43 + </el-col>
  44 + <el-col :span="12">
  45 + <el-form-item label="申请门店" prop="applicationStoreId">
  46 + <el-select
  47 + v-model="dataForm.applicationStoreId"
  48 + placeholder="请选择申请门店"
  49 + clearable
  50 + filterable
  51 + style="width: 100%"
  52 + >
  53 + <el-option
  54 + v-for="store in storeOptions"
  55 + :key="store.id"
  56 + :label="store.dm"
  57 + :value="store.id"
  58 + />
  59 + </el-select>
  60 + </el-form-item>
  61 + </el-col>
  62 + <el-col :span="12">
  63 + <el-form-item label="申请时间" prop="applicationTime">
  64 + <el-date-picker
  65 + v-model="dataForm.applicationTime"
  66 + type="datetime"
  67 + placeholder="请选择申请时间"
  68 + format="yyyy-MM-dd HH:mm:ss"
  69 + value-format="timestamp"
  70 + style="width: 100%"
  71 + />
  72 + </el-form-item>
  73 + </el-col>
  74 + <el-col :span="12">
  75 + <el-form-item label="总金额" prop="amount">
  76 + <el-input
  77 + v-model="dataForm.amount"
  78 + placeholder="自动计算"
  79 + readonly
  80 + >
  81 + <template slot="prepend">¥</template>
  82 + </el-input>
  83 + </el-form-item>
  84 + </el-col>
  85 + </el-row>
  86 + </el-card>
  87 +
  88 + <!-- 选择购买记录 -->
  89 + <el-card class="form-card" shadow="hover">
  90 + <div slot="header" class="card-header">
  91 + <i class="el-icon-shopping-cart-full"></i>
  92 + <span class="card-title">选择购买记录</span>
  93 + </div>
  94 + <div>
  95 + <el-button
  96 + type="primary"
  97 + icon="el-icon-plus"
  98 + @click="openPurchaseDialog"
  99 + >
  100 + 选择购买记录
  101 + </el-button>
  102 + <span v-if="selectedPurchaseRecords.length > 0" class="selected-count">
  103 + 已选择 {{ selectedPurchaseRecords.length }} 条记录
  104 + </span>
  105 + </div>
  106 +
  107 + <!-- 已选择的购买记录列表 -->
  108 + <div v-if="selectedPurchaseRecords.length > 0" class="purchase-list">
  109 + <el-table :data="selectedPurchaseRecords" border size="small">
  110 + <el-table-column prop="reimbursementCategoryName" label="分类名称" />
  111 + <el-table-column prop="unitPrice" label="单价" >
  112 + <template slot-scope="scope">
  113 + ¥{{ formatMoney(scope.row.unitPrice) }}
  114 + </template>
  115 + </el-table-column>
  116 + <el-table-column prop="quantity" label="数量" />
  117 + <el-table-column prop="amount" label="金额" >
  118 + <template slot-scope="scope">
  119 + ¥{{ formatMoney(scope.row.amount) }}
  120 + </template>
  121 + </el-table-column>
  122 + <el-table-column prop="purchaseTime" label="购买时间" width="180">
  123 + <template slot-scope="scope">
  124 + {{ formatDateTime(scope.row.purchaseTime) }}
  125 + </template>
  126 + </el-table-column>
  127 + <el-table-column label="操作" width="100" align="center">
  128 + <template slot-scope="scope">
  129 + <el-button
  130 + v-if="!scope.row.isInitial"
  131 + type="text"
  132 + icon="el-icon-delete"
  133 + @click="removePurchaseRecord(scope.$index)"
  134 + style="color: #F56C6C"
  135 + >
  136 + 移除
  137 + </el-button>
  138 + <span v-else style="color: #909399; font-size: 12px;">初始记录</span>
  139 + </template>
  140 + </el-table-column>
  141 + </el-table>
  142 + </div>
  143 + </el-card>
  144 + </el-form>
  145 + </div>
  146 + <span slot="footer" class="dialog-footer">
  147 + <el-button @click="visible = false">取 消</el-button>
  148 + <el-button
  149 + type="primary"
  150 + @click="handleSubmit"
  151 + :loading="submitting"
  152 + >
  153 + 保存修改
  154 + </el-button>
  155 + </span>
  156 +
  157 + <!-- 选择购买记录弹窗 -->
  158 + <el-dialog
  159 + title="选择购买记录"
  160 + :visible.sync="showPurchaseDialog"
  161 + width="900px"
  162 + append-to-body
  163 + >
  164 + <div class="purchase-dialog-content">
  165 + <div class="purchase-search">
  166 + <el-input
  167 + v-model="purchaseSearchKeyword"
  168 + placeholder="搜索分类名称"
  169 + clearable
  170 + @input="handlePurchaseSearch"
  171 + style="width: 300px"
  172 + >
  173 + <el-button slot="append" icon="el-icon-search" @click="loadPurchaseRecords" />
  174 + </el-input>
  175 + </div>
  176 + <el-table
  177 + :data="purchaseRecordList"
  178 + v-loading="purchaseLoading"
  179 + @selection-change="handlePurchaseSelectionChange"
  180 + border
  181 + max-height="400"
  182 + >
  183 + <el-table-column type="selection" width="55"/>
  184 + <el-table-column prop="reimbursementCategoryName" label="分类名称" />
  185 + <el-table-column prop="unitPrice" label="单价" >
  186 + <template slot-scope="scope">
  187 + ¥{{ formatMoney(scope.row.unitPrice) }}
  188 + </template>
  189 + </el-table-column>
  190 + <el-table-column prop="quantity" label="数量" />
  191 + <el-table-column prop="amount" label="金额" >
  192 + <template slot-scope="scope">
  193 + ¥{{ formatMoney(scope.row.amount) }}
  194 + </template>
  195 + </el-table-column>
  196 + <el-table-column prop="purchaseTime" label="购买时间" width="180">
  197 + <template slot-scope="scope">
  198 + {{ formatDateTime(scope.row.purchaseTime) }}
  199 + </template>
  200 + </el-table-column>
  201 + </el-table>
  202 + <div class="purchase-pagination">
  203 + <el-pagination
  204 + @size-change="handlePurchaseSizeChange"
  205 + @current-change="handlePurchaseCurrentChange"
  206 + :current-page="purchaseQuery.currentPage"
  207 + :page-sizes="[10, 20, 50]"
  208 + :page-size="purchaseQuery.pageSize"
  209 + layout="total, sizes, prev, pager, next"
  210 + :total="purchaseTotal"
  211 + />
  212 + </div>
  213 + </div>
  214 + <span slot="footer" class="dialog-footer">
  215 + <el-button @click="showPurchaseDialog = false">取 消</el-button>
  216 + <el-button type="primary" @click="confirmPurchaseSelection">确 定</el-button>
  217 + </span>
  218 + </el-dialog>
  219 + </el-dialog>
  220 +</template>
  221 +
  222 +<script>
  223 +import request from '@/utils/request'
  224 +
  225 +export default {
  226 + name: 'EditDialog',
  227 + data() {
  228 + return {
  229 + visible: false,
  230 + loading: false,
  231 + submitting: false,
  232 + applicationId: null,
  233 + storeOptions: [],
  234 + dataForm: {
  235 + applicationUserId: '',
  236 + applicationUserName: '',
  237 + applicationStoreId: '',
  238 + applicationTime: null,
  239 + amount: '0.00',
  240 + selectedPurchaseRecordIds: []
  241 + },
  242 + rules: {
  243 + applicationStoreId: [
  244 + { required: true, message: '请选择申请门店', trigger: 'change' }
  245 + ],
  246 + applicationTime: [
  247 + { required: true, message: '请选择申请时间', trigger: 'change' }
  248 + ],
  249 + amount: [
  250 + { required: true, message: '总金额不能为空', trigger: 'blur' }
  251 + ]
  252 + },
  253 + selectedPurchaseRecords: [],
  254 + showPurchaseDialog: false,
  255 + purchaseRecordList: [],
  256 + purchaseLoading: false,
  257 + purchaseSearchKeyword: '',
  258 + purchaseQuery: {
  259 + currentPage: 1,
  260 + pageSize: 20,
  261 + reimbursementCategoryName: ''
  262 + },
  263 + purchaseTotal: 0,
  264 + purchaseSelection: [],
  265 + initialPurchaseRecordIds: [] // 存储初始的购买记录ID列表
  266 + }
  267 + },
  268 + created() {
  269 + this.loadStoreOptions()
  270 + },
  271 + methods: {
  272 + async init(id) {
  273 + this.visible = true
  274 + this.loading = true
  275 + this.applicationId = id
  276 + this.submitting = false
  277 +
  278 + // 重置表单
  279 + this.dataForm = {
  280 + applicationUserId: '',
  281 + applicationUserName: '',
  282 + applicationStoreId: '',
  283 + applicationTime: null,
  284 + amount: '0.00',
  285 + selectedPurchaseRecordIds: []
  286 + }
  287 + this.selectedPurchaseRecords = []
  288 + this.initialPurchaseRecordIds = [] // 重置初始记录ID列表
  289 +
  290 + // 加载详情数据
  291 + await this.loadDetail(id)
  292 + },
  293 +
  294 + async loadDetail(id) {
  295 + try {
  296 + const response = await request({
  297 + url: `/api/Extend/LqReimbursementApplication/${id}`,
  298 + method: 'GET'
  299 + })
  300 +
  301 + if (response.code === 200 && response.data && response.data.form) {
  302 + const formData = response.data.form
  303 + const purchaseRecords = response.data.purchaseRecords || []
  304 +
  305 + // 填充表单数据
  306 + this.dataForm = {
  307 + applicationUserId: formData.applicationUserId || '',
  308 + applicationUserName: formData.applicationUserName || '',
  309 + applicationStoreId: formData.applicationStoreId || '',
  310 + applicationTime: formData.applicationTime || null,
  311 + amount: formData.amount || '0.00',
  312 + selectedPurchaseRecordIds: purchaseRecords.map(record => record.id)
  313 + }
  314 +
  315 + // 保存初始的购买记录ID列表
  316 + this.initialPurchaseRecordIds = purchaseRecords.map(record => record.id)
  317 +
  318 + // 填充购买记录列表,标记初始记录
  319 + this.selectedPurchaseRecords = purchaseRecords.map(record => {
  320 + // 处理附件字段
  321 + if (record.attachment) {
  322 + try {
  323 + record.attachment = typeof record.attachment === 'string'
  324 + ? JSON.parse(record.attachment)
  325 + : record.attachment
  326 + } catch (e) {
  327 + record.attachment = []
  328 + }
  329 + } else {
  330 + record.attachment = []
  331 + }
  332 + // 标记为初始记录
  333 + record.isInitial = true
  334 + return record
  335 + })
  336 +
  337 + // 计算总金额
  338 + this.calculateAmount()
  339 + } else {
  340 + this.$message.error(response.msg || '获取详情失败')
  341 + this.visible = false
  342 + }
  343 + } catch (error) {
  344 + console.error('获取详情失败:', error)
  345 + this.$message.error('获取详情失败')
  346 + this.visible = false
  347 + } finally {
  348 + this.loading = false
  349 + }
  350 + },
  351 +
  352 + async loadStoreOptions() {
  353 + try {
  354 + const response = await request({
  355 + url: '/api/Extend/LqMdxx',
  356 + method: 'GET',
  357 + data: {
  358 + currentPage: 1,
  359 + pageSize: 1000
  360 + }
  361 + })
  362 + if (response.code === 200 && response.data && response.data.list) {
  363 + this.storeOptions = response.data.list
  364 + }
  365 + } catch (error) {
  366 + console.error('加载门店列表失败:', error)
  367 + }
  368 + },
  369 +
  370 + // 申请人改变时,自动填充申请人姓名
  371 + handleUserChange(userId) {
  372 + if (userId) {
  373 + // 处理userId可能是字符串(逗号分隔)的情况,取第一个
  374 + const userIdArray = typeof userId === 'string'
  375 + ? userId.split(',').filter(id => id && id.trim())
  376 + : [userId]
  377 + const actualUserId = userIdArray.length > 0 ? userIdArray[0] : userId
  378 +
  379 + // 获取用户信息
  380 + request({
  381 + url: '/api/permission/Users/' + actualUserId,
  382 + method: 'GET',
  383 + }).then(res => {
  384 + if (res.code === 200 && res.data) {
  385 + this.dataForm.applicationUserName = res.data.realName || res.data.userName || res.data.fullName || ''
  386 + // 如果门店为空,自动填充用户的门店
  387 + if (!this.dataForm.applicationStoreId && res.data.mdid) {
  388 + this.dataForm.applicationStoreId = res.data.mdid
  389 + }
  390 + }
  391 + }).catch(error => {
  392 + console.error('获取用户信息失败:', error)
  393 + })
  394 + } else {
  395 + // 清空申请人时,也清空姓名
  396 + this.dataForm.applicationUserName = ''
  397 + }
  398 + },
  399 +
  400 + openPurchaseDialog() {
  401 + this.showPurchaseDialog = true
  402 + this.loadPurchaseRecords()
  403 + },
  404 +
  405 + async loadPurchaseRecords() {
  406 + this.purchaseLoading = true
  407 + try {
  408 + const params = {
  409 + currentPage: this.purchaseQuery.currentPage,
  410 + pageSize: this.purchaseQuery.pageSize,
  411 + approveStatus: '未审批', // 只选择未审批的购买记录
  412 + createUserStoreId: this.dataForm.applicationStoreId || '暂无',
  413 + }
  414 + if (this.purchaseSearchKeyword) {
  415 + params.reimbursementCategoryName = this.purchaseSearchKeyword
  416 + }
  417 + const response = await request({
  418 + url: '/api/Extend/LqPurchaseRecords',
  419 + method: 'GET',
  420 + data: params
  421 + })
  422 + if (response.code === 200 && response.data) {
  423 + this.purchaseRecordList = response.data.list || []
  424 + this.purchaseTotal = (response.data.pagination && response.data.pagination.Total) || 0
  425 + }
  426 + } catch (error) {
  427 + console.error('加载购买记录失败:', error)
  428 + this.$message.error('加载购买记录失败')
  429 + } finally {
  430 + this.purchaseLoading = false
  431 + }
  432 + },
  433 +
  434 + handlePurchaseSearch() {
  435 + this.purchaseQuery.currentPage = 1
  436 + this.loadPurchaseRecords()
  437 + },
  438 +
  439 + handlePurchaseSelectionChange(selection) {
  440 + this.purchaseSelection = selection
  441 + },
  442 +
  443 + confirmPurchaseSelection() {
  444 + // 合并新选择的记录,标记为非初始记录
  445 + const newRecords = this.purchaseSelection.filter(item => {
  446 + return !this.selectedPurchaseRecords.find(record => record.id === item.id)
  447 + }).map(record => {
  448 + // 标记新增的记录
  449 + record.isInitial = false
  450 + return record
  451 + })
  452 + this.selectedPurchaseRecords = [...this.selectedPurchaseRecords, ...newRecords]
  453 + this.calculateAmount()
  454 + this.showPurchaseDialog = false
  455 + // 清空选择
  456 + this.purchaseSelection = []
  457 + },
  458 +
  459 + removePurchaseRecord(index) {
  460 + const record = this.selectedPurchaseRecords[index]
  461 + // 检查是否是初始记录,初始记录不允许移除
  462 + if (record && record.isInitial) {
  463 + this.$message.warning('初始购买记录不能移除')
  464 + return
  465 + }
  466 + this.selectedPurchaseRecords.splice(index, 1)
  467 + this.calculateAmount()
  468 + },
  469 +
  470 + calculateAmount() {
  471 + // 计算总金额
  472 + let total = 0
  473 + this.selectedPurchaseRecords.forEach(record => {
  474 + total += Number(record.amount || 0)
  475 + })
  476 + this.dataForm.amount = total.toFixed(2)
  477 + this.dataForm.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(record => record.id)
  478 + },
  479 +
  480 + handlePurchaseSizeChange(val) {
  481 + this.purchaseQuery.pageSize = val
  482 + this.loadPurchaseRecords()
  483 + },
  484 +
  485 + handlePurchaseCurrentChange(val) {
  486 + this.purchaseQuery.currentPage = val
  487 + this.loadPurchaseRecords()
  488 + },
  489 +
  490 + async handleSubmit() {
  491 + // 验证表单
  492 + this.$refs.form.validate(async (valid) => {
  493 + if (!valid) {
  494 + return false
  495 + }
  496 +
  497 + // 验证购买记录
  498 + if (!this.selectedPurchaseRecords || this.selectedPurchaseRecords.length === 0) {
  499 + this.$message.warning('请至少选择一条购买记录')
  500 + return false
  501 + }
  502 +
  503 + this.submitting = true
  504 + try {
  505 + // 准备提交数据(只传递有值的字段)
  506 + const submitData = {
  507 + id: this.applicationId
  508 + }
  509 +
  510 + // 只添加有值的字段
  511 + if (this.dataForm.applicationUserId) {
  512 + submitData.applicationUserId = this.dataForm.applicationUserId
  513 + }
  514 + if (this.dataForm.applicationUserName) {
  515 + submitData.applicationUserName = this.dataForm.applicationUserName
  516 + }
  517 + if (this.dataForm.applicationStoreId) {
  518 + submitData.applicationStoreId = this.dataForm.applicationStoreId
  519 + }
  520 + if (this.dataForm.applicationTime) {
  521 + submitData.applicationTime = this.dataForm.applicationTime
  522 + }
  523 + if (this.dataForm.amount) {
  524 + submitData.amount = this.dataForm.amount
  525 + }
  526 + if (this.dataForm.selectedPurchaseRecordIds && this.dataForm.selectedPurchaseRecordIds.length > 0) {
  527 + submitData.selectedPurchaseRecordIds = this.dataForm.selectedPurchaseRecordIds
  528 + }
  529 +
  530 + // 调用 PUT 接口更新数据
  531 + const response = await request({
  532 + url: `/api/Extend/LqReimbursementApplication/${this.applicationId}`,
  533 + method: 'PUT',
  534 + data: submitData
  535 + })
  536 +
  537 + if (response.code === 200) {
  538 + this.$message.success('修改成功')
  539 +
  540 + // 修改成功后,自动重新提交审批
  541 + try {
  542 + const submitResponse = await request({
  543 + url: `/api/Extend/LqReimbursementApplication/${this.applicationId}/Actions/SubmitApproval`,
  544 + method: 'POST'
  545 + })
  546 +
  547 + if (submitResponse.code === 200) {
  548 + this.$message.success('已重新提交审批')
  549 + } else {
  550 + this.$message.warning('修改成功,但重新提交审批失败:' + (submitResponse.msg || '未知错误'))
  551 + }
  552 + } catch (error) {
  553 + console.error('重新提交审批失败:', error)
  554 + this.$message.warning('修改成功,但重新提交审批失败,请手动提交')
  555 + }
  556 +
  557 + this.visible = false
  558 + this.$emit('refresh')
  559 + } else {
  560 + this.$message.error(response.msg || '修改失败')
  561 + }
  562 + } catch (error) {
  563 + console.error('修改失败:', error)
  564 + this.$message.error('修改失败')
  565 + } finally {
  566 + this.submitting = false
  567 + }
  568 + })
  569 + },
  570 +
  571 + handleClose() {
  572 + this.visible = false
  573 + this.$refs.form && this.$refs.form.resetFields()
  574 + this.dataForm = {
  575 + applicationUserId: '',
  576 + applicationUserName: '',
  577 + applicationStoreId: '',
  578 + applicationTime: null,
  579 + amount: '0.00',
  580 + selectedPurchaseRecordIds: []
  581 + }
  582 + this.selectedPurchaseRecords = []
  583 + this.initialPurchaseRecordIds = []
  584 + this.applicationId = null
  585 + },
  586 +
  587 + formatMoney(amount) {
  588 + if (!amount) return '0.00'
  589 + return Number(amount).toLocaleString('zh-CN', {
  590 + minimumFractionDigits: 2,
  591 + maximumFractionDigits: 2
  592 + })
  593 + },
  594 +
  595 + formatDateTime(dateTime) {
  596 + if (!dateTime) return '无'
  597 + const date = new Date(dateTime)
  598 + const year = date.getFullYear()
  599 + const month = String(date.getMonth() + 1).padStart(2, '0')
  600 + const day = String(date.getDate()).padStart(2, '0')
  601 + const hours = String(date.getHours()).padStart(2, '0')
  602 + const minutes = String(date.getMinutes()).padStart(2, '0')
  603 + return `${year}-${month}-${day} ${hours}:${minutes}`
  604 + }
  605 + }
  606 +}
  607 +</script>
  608 +
  609 +<style lang="scss" scoped>
  610 +.form-content {
  611 + overflow-y: auto;
  612 + padding: 0 10px;
  613 +}
  614 +
  615 +.form-card {
  616 + margin-bottom: 20px;
  617 +
  618 + .card-header {
  619 + display: flex;
  620 + align-items: center;
  621 + gap: 8px;
  622 + font-weight: 600;
  623 + color: #303133;
  624 +
  625 + i {
  626 + font-size: 18px;
  627 + color: #409EFF;
  628 + }
  629 +
  630 + .card-title {
  631 + flex: 1;
  632 + }
  633 + }
  634 +}
  635 +
  636 +.selected-count {
  637 + margin-left: 12px;
  638 + color: #67C23A;
  639 + font-size: 14px;
  640 +}
  641 +
  642 +.purchase-list {
  643 + margin-top: 16px;
  644 +}
  645 +
  646 +.purchase-dialog-content {
  647 + .purchase-search {
  648 + margin-bottom: 16px;
  649 + }
  650 +
  651 + .purchase-pagination {
  652 + margin-top: 16px;
  653 + text-align: right;
  654 + }
  655 +}
  656 +</style>
  657 +
... ...
antis-ncc-admin/src/views/lqReimbursementApplication/form-dialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="创建报销申请"
  4 + :visible.sync="visible"
  5 + :close-on-click-modal="false"
  6 + class="NCC-dialog NCC-dialog_center"
  7 + width="1200px"
  8 + @close="handleClose"
  9 + >
  10 + <div class="form-content" v-loading="loading">
  11 + <el-form
  12 + ref="form"
  13 + :model="dataForm"
  14 + :rules="rules"
  15 + label-width="120px"
  16 + label-position="right"
  17 + >
  18 + <!-- 基本信息 -->
  19 + <el-card class="form-card" shadow="hover">
  20 + <div slot="header" class="card-header">
  21 + <i class="el-icon-document"></i>
  22 + <span class="card-title">基本信息</span>
  23 + </div>
  24 + <el-row :gutter="20">
  25 + <el-col :span="12">
  26 + <el-form-item label="申请人" prop="applicationUserId">
  27 + <user-select
  28 + v-model="dataForm.applicationUserId"
  29 + placeholder="请选择申请人"
  30 + clearable
  31 + @change="handleUserChange"
  32 + />
  33 + </el-form-item>
  34 + </el-col>
  35 + <el-col :span="12">
  36 + <el-form-item label="申请人姓名" prop="applicationUserName">
  37 + <el-input
  38 + v-model="dataForm.applicationUserName"
  39 + placeholder="自动填充"
  40 + readonly
  41 + />
  42 + </el-form-item>
  43 + </el-col>
  44 + <el-col :span="12">
  45 + <el-form-item label="申请门店" prop="applicationStoreId">
  46 + <el-select
  47 + v-model="dataForm.applicationStoreId"
  48 + placeholder="自动填充"
  49 + disabled
  50 + style="width: 100%"
  51 + >
  52 + <el-option
  53 + v-for="store in storeOptions"
  54 + :key="store.id"
  55 + :label="store.dm"
  56 + :value="store.id"
  57 + />
  58 + </el-select>
  59 + </el-form-item>
  60 + </el-col>
  61 + <el-col :span="12">
  62 + <el-form-item label="申请时间" prop="applicationTime">
  63 + <el-date-picker
  64 + v-model="dataForm.applicationTime"
  65 + type="datetime"
  66 + placeholder="请选择申请时间"
  67 + format="yyyy-MM-dd HH:mm:ss"
  68 + value-format="timestamp"
  69 + style="width: 100%"
  70 + />
  71 + </el-form-item>
  72 + </el-col>
  73 + <el-col :span="12">
  74 + <el-form-item label="总金额" prop="amount">
  75 + <el-input
  76 + v-model="dataForm.amount"
  77 + placeholder="自动计算"
  78 + readonly
  79 + >
  80 + <template slot="prepend">¥</template>
  81 + </el-input>
  82 + </el-form-item>
  83 + </el-col>
  84 + </el-row>
  85 + </el-card>
  86 +
  87 + <!-- 选择购买记录 -->
  88 + <el-card class="form-card" shadow="hover">
  89 + <div slot="header" class="card-header">
  90 + <i class="el-icon-shopping-cart-full"></i>
  91 + <span class="card-title">选择购买记录</span>
  92 + </div>
  93 + <div>
  94 + <el-button
  95 + type="primary"
  96 + icon="el-icon-plus"
  97 + @click="openPurchaseDialog"
  98 + >
  99 + 选择购买记录
  100 + </el-button>
  101 + <span v-if="selectedPurchaseRecords.length > 0" class="selected-count">
  102 + 已选择 {{ selectedPurchaseRecords.length }} 条记录
  103 + </span>
  104 + </div>
  105 +
  106 + <!-- 已选择的购买记录列表 -->
  107 + <div v-if="selectedPurchaseRecords.length > 0" class="purchase-list">
  108 + <el-table :data="selectedPurchaseRecords" border size="small">
  109 + <el-table-column prop="reimbursementCategoryName" label="分类名称" />
  110 + <el-table-column prop="unitPrice" label="单价" >
  111 + <template slot-scope="scope">
  112 + ¥{{ formatMoney(scope.row.unitPrice) }}
  113 + </template>
  114 + </el-table-column>
  115 + <el-table-column prop="quantity" label="数量" />
  116 + <el-table-column prop="amount" label="金额" >
  117 + <template slot-scope="scope">
  118 + ¥{{ formatMoney(scope.row.amount) }}
  119 + </template>
  120 + </el-table-column>
  121 + <el-table-column prop="purchaseTime" label="购买时间" width="180">
  122 + <template slot-scope="scope">
  123 + {{ formatDateTime(scope.row.purchaseTime) }}
  124 + </template>
  125 + </el-table-column>
  126 + <el-table-column label="操作" width="100" align="center">
  127 + <template slot-scope="scope">
  128 + <el-button
  129 + type="text"
  130 + icon="el-icon-delete"
  131 + @click="removePurchaseRecord(scope.$index)"
  132 + style="color: #F56C6C"
  133 + >
  134 + 移除
  135 + </el-button>
  136 + </template>
  137 + </el-table-column>
  138 + </el-table>
  139 + </div>
  140 + </el-card>
  141 +
  142 + <!-- 审批节点配置 -->
  143 + <el-card class="form-card" shadow="hover">
  144 + <div slot="header" class="card-header">
  145 + <i class="el-icon-s-order"></i>
  146 + <span class="card-title">审批节点配置(1-5个节点)</span>
  147 + <el-button
  148 + type="text"
  149 + icon="el-icon-plus"
  150 + @click="addNode"
  151 + style="margin-left: auto;"
  152 + :disabled="dataForm.nodes.length >= 5"
  153 + >
  154 + 添加节点
  155 + </el-button>
  156 + </div>
  157 + <div class="nodes-list">
  158 + <div
  159 + v-for="(node, index) in dataForm.nodes"
  160 + :key="index"
  161 + class="node-item"
  162 + >
  163 + <div class="node-header">
  164 + <span class="node-order">节点 {{ node.nodeOrder }}</span>
  165 + <el-button
  166 + type="text"
  167 + icon="el-icon-delete"
  168 + @click="removeNode(index)"
  169 + style="color: #F56C6C"
  170 + :disabled="dataForm.nodes.length <= 1"
  171 + >
  172 + 删除
  173 + </el-button>
  174 + </div>
  175 + <el-row :gutter="20">
  176 + <el-col :span="12">
  177 + <el-form-item
  178 + :label="`节点${node.nodeOrder}名称`"
  179 + :prop="`nodes.${index}.nodeName`"
  180 + :rules="{ required: true, message: '请输入节点名称', trigger: 'blur' }"
  181 + >
  182 + <el-input
  183 + v-model="node.nodeName"
  184 + placeholder="请输入节点名称"
  185 + />
  186 + </el-form-item>
  187 + </el-col>
  188 + <el-col :span="12">
  189 + <el-form-item
  190 + :label="`节点${node.nodeOrder}类型`"
  191 + :prop="`nodes.${index}.approvalType`"
  192 + :rules="{ required: true, message: '请选择审批类型', trigger: 'change' }"
  193 + >
  194 + <el-select
  195 + v-model="node.approvalType"
  196 + placeholder="请选择审批类型"
  197 + style="width: 100%"
  198 + >
  199 + <el-option label="会签" value="会签" />
  200 + <el-option label="或签" value="或签" />
  201 + </el-select>
  202 + </el-form-item>
  203 + </el-col>
  204 + <el-col :span="24">
  205 + <el-form-item
  206 + :label="`节点${node.nodeOrder}审批人`"
  207 + :prop="`nodes.${index}.approverIds`"
  208 + :rules="{ required: true, message: '请选择至少一个审批人', trigger: 'change' }"
  209 + >
  210 + <user-select
  211 + v-model="node.approverIds"
  212 + placeholder="请选择审批人"
  213 + :multiple="true"
  214 + @change="(val) => handleApproverChange(index, val)"
  215 + style="width: 100%"
  216 + />
  217 + </el-form-item>
  218 + </el-col>
  219 + <el-col :span="24" v-if="node.approverNames && node.approverNames.length > 0">
  220 + <el-form-item label="已选审批人">
  221 + <el-tag
  222 + v-for="(name, nameIndex) in node.approverNames"
  223 + :key="nameIndex"
  224 + style="margin-right: 8px;"
  225 + >
  226 + {{ name }}
  227 + </el-tag>
  228 + </el-form-item>
  229 + </el-col>
  230 + </el-row>
  231 + </div>
  232 + <div v-if="dataForm.nodes.length === 0" class="empty-nodes">
  233 + <el-empty description="请添加审批节点(至少1个)" :image-size="80" />
  234 + </div>
  235 + </div>
  236 + </el-card>
  237 + </el-form>
  238 + </div>
  239 + <span slot="footer" class="dialog-footer">
  240 + <el-button @click="visible = false">取 消</el-button>
  241 + <el-button
  242 + type="primary"
  243 + @click="handleSubmit"
  244 + :loading="submitting"
  245 + >
  246 + 创建并提交审批
  247 + </el-button>
  248 + </span>
  249 +
  250 + <!-- 选择购买记录弹窗 -->
  251 + <el-dialog
  252 + title="选择购买记录"
  253 + :visible.sync="showPurchaseDialog"
  254 + width="900px"
  255 + append-to-body
  256 + >
  257 + <div class="purchase-dialog-content">
  258 + <div class="purchase-search">
  259 + <el-input
  260 + v-model="purchaseSearchKeyword"
  261 + placeholder="搜索分类名称"
  262 + clearable
  263 + @input="handlePurchaseSearch"
  264 + style="width: 300px"
  265 + >
  266 + <el-button slot="append" icon="el-icon-search" @click="loadPurchaseRecords" />
  267 + </el-input>
  268 + </div>
  269 + <el-table
  270 + :data="purchaseRecordList"
  271 + v-loading="purchaseLoading"
  272 + @selection-change="handlePurchaseSelectionChange"
  273 + border
  274 + max-height="400"
  275 + >
  276 + <el-table-column type="selection" width="55"/>
  277 + <el-table-column prop="reimbursementCategoryName" label="分类名称" />
  278 + <el-table-column prop="unitPrice" label="单价" >
  279 + <template slot-scope="scope">
  280 + ¥{{ formatMoney(scope.row.unitPrice) }}
  281 + </template>
  282 + </el-table-column>
  283 + <el-table-column prop="quantity" label="数量" />
  284 + <el-table-column prop="amount" label="金额" >
  285 + <template slot-scope="scope">
  286 + ¥{{ formatMoney(scope.row.amount) }}
  287 + </template>
  288 + </el-table-column>
  289 + <el-table-column prop="purchaseTime" label="购买时间" width="180">
  290 + <template slot-scope="scope">
  291 + {{ formatDateTime(scope.row.purchaseTime) }}
  292 + </template>
  293 + </el-table-column>
  294 + </el-table>
  295 + <div class="purchase-pagination">
  296 + <el-pagination
  297 + @size-change="handlePurchaseSizeChange"
  298 + @current-change="handlePurchaseCurrentChange"
  299 + :current-page="purchaseQuery.currentPage"
  300 + :page-sizes="[10, 20, 50]"
  301 + :page-size="purchaseQuery.pageSize"
  302 + layout="total, sizes, prev, pager, next"
  303 + :total="purchaseTotal"
  304 + />
  305 + </div>
  306 + </div>
  307 + <span slot="footer" class="dialog-footer">
  308 + <el-button @click="showPurchaseDialog = false">取 消</el-button>
  309 + <el-button type="primary" @click="confirmPurchaseSelection">确 定</el-button>
  310 + </span>
  311 + </el-dialog>
  312 + </el-dialog>
  313 +</template>
  314 +
  315 +<script>
  316 +import request from '@/utils/request'
  317 +import { getUserInfoList } from '@/api/permission/user'
  318 +
  319 +export default {
  320 + name: 'FormDialog',
  321 + data() {
  322 + return {
  323 + visible: false,
  324 + loading: false,
  325 + submitting: false,
  326 + storeOptions: [],
  327 + dataForm: {
  328 + applicationUserId: '',
  329 + applicationUserName: '',
  330 + applicationStoreId: '',
  331 + applicationTime: null,
  332 + amount: '0.00',
  333 + selectedPurchaseRecordIds: [],
  334 + nodes: []
  335 + },
  336 + rules: {
  337 + applicationStoreId: [
  338 + { required: true, message: '请选择申请门店', trigger: 'change' }
  339 + ],
  340 + applicationTime: [
  341 + { required: true, message: '请选择申请时间', trigger: 'change' }
  342 + ],
  343 + amount: [
  344 + { required: true, message: '总金额不能为空', trigger: 'blur' }
  345 + ],
  346 + selectedPurchaseRecordIds: [
  347 + { required: true, message: '请至少选择一条购买记录', trigger: 'change', validator: this.validatePurchaseRecords }
  348 + ]
  349 + },
  350 + selectedPurchaseRecords: [],
  351 + showPurchaseDialog: false,
  352 + purchaseRecordList: [],
  353 + purchaseLoading: false,
  354 + purchaseSearchKeyword: '',
  355 + purchaseQuery: {
  356 + currentPage: 1,
  357 + pageSize: 20,
  358 + reimbursementCategoryName: ''
  359 + },
  360 + purchaseTotal: 0,
  361 + purchaseSelection: []
  362 + }
  363 + },
  364 + created() {
  365 + this.loadStoreOptions()
  366 + // 初始化1个节点
  367 + this.initNodes()
  368 + },
  369 + methods: {
  370 + openPurchaseDialog() {
  371 + this.showPurchaseDialog = true
  372 + this.loadPurchaseRecords()
  373 + },
  374 + async init() {
  375 + this.visible = true
  376 + this.loading = false
  377 + this.submitting = false
  378 +
  379 + // 获取当前登录用户信息
  380 + const currentUser = this.$store.getters.userInfo
  381 + const defaultUserId = currentUser && (currentUser.userId || currentUser.id) ? (currentUser.userId || currentUser.id) : ''
  382 + const defaultUserName = currentUser && (currentUser.realName || currentUser.fullName || currentUser.userName) ? (currentUser.realName || currentUser.fullName || currentUser.userName) : ''
  383 +
  384 + this.dataForm = {
  385 + applicationUserId: defaultUserId,
  386 + applicationUserName: defaultUserName,
  387 + applicationStoreId: '',
  388 + applicationTime: new Date().getTime(),
  389 + amount: '0.00',
  390 + selectedPurchaseRecordIds: [],
  391 + nodes: []
  392 + }
  393 + this.selectedPurchaseRecords = []
  394 + this.initNodes()
  395 +
  396 + // 如果有默认用户,自动填充门店
  397 + if (defaultUserId) {
  398 + this.handleUserChange(defaultUserId)
  399 + }
  400 + },
  401 +
  402 + initNodes() {
  403 + // 初始化1个节点
  404 + this.dataForm.nodes = [
  405 + {
  406 + nodeOrder: 1,
  407 + nodeName: '',
  408 + approvalType: '',
  409 + approverIds: '', // user-select组件期望字符串,不是数组
  410 + approverNames: []
  411 + }
  412 + ]
  413 + },
  414 +
  415 + async loadStoreOptions() {
  416 + try {
  417 + const response = await request({
  418 + url: '/api/Extend/LqMdxx',
  419 + method: 'GET',
  420 + data: {
  421 + currentPage: 1,
  422 + pageSize: 1000
  423 + }
  424 + })
  425 + if (response.code === 200 && response.data && response.data.list) {
  426 + this.storeOptions = response.data.list
  427 + }
  428 + } catch (error) {
  429 + console.error('加载门店列表失败:', error)
  430 + }
  431 + },
  432 +
  433 + // 申请人改变时,自动填充申请人姓名和申请门店
  434 + handleUserChange(userId) {
  435 + if (userId) {
  436 + // 处理userId可能是字符串(逗号分隔)的情况,取第一个
  437 + const userIdArray = typeof userId === 'string'
  438 + ? userId.split(',').filter(id => id && id.trim())
  439 + : [userId]
  440 + const actualUserId = userIdArray.length > 0 ? userIdArray[0] : userId
  441 +
  442 + // 获取用户信息
  443 + request({
  444 + url: '/api/permission/Users/' + actualUserId,
  445 + method: 'GET',
  446 + }).then(res => {
  447 + if (res.code === 200 && res.data) {
  448 + this.dataForm.applicationUserName = res.data.realName || res.data.userName || res.data.fullName || ''
  449 + this.dataForm.applicationStoreId = res.data.mdid || ''
  450 + }
  451 + }).catch(error => {
  452 + console.error('获取用户信息失败:', error)
  453 + })
  454 + } else {
  455 + // 清空申请人时,也清空姓名和门店
  456 + this.dataForm.applicationUserName = ''
  457 + this.dataForm.applicationStoreId = ''
  458 + }
  459 + },
  460 +
  461 + addNode() {
  462 + if (this.dataForm.nodes.length >= 5) {
  463 + this.$message.warning('最多只能添加5个节点')
  464 + return
  465 + }
  466 + const nextOrder = this.dataForm.nodes.length + 1
  467 + this.dataForm.nodes.push({
  468 + nodeOrder: nextOrder,
  469 + nodeName: '',
  470 + approvalType: '',
  471 + approverIds: '', // user-select组件期望字符串,不是数组
  472 + approverNames: []
  473 + })
  474 + },
  475 +
  476 + removeNode(index) {
  477 + if (this.dataForm.nodes.length <= 1) {
  478 + this.$message.warning('至少需要1个节点')
  479 + return
  480 + }
  481 + this.dataForm.nodes.splice(index, 1)
  482 + // 重新排序
  483 + this.dataForm.nodes.forEach((node, idx) => {
  484 + node.nodeOrder = idx + 1
  485 + })
  486 + },
  487 +
  488 + async handleApproverChange(index, userIds) {
  489 + const node = this.dataForm.nodes[index]
  490 +
  491 + // user-select组件返回的是字符串(逗号分隔的ID)
  492 + // 但我们需要保存为字符串格式,因为组件期望字符串
  493 + const idsString = userIds || ''
  494 + node.approverIds = idsString
  495 +
  496 + // 转换为数组用于获取用户名
  497 + const idsArray = idsString ? idsString.split(',').filter(id => id && id.trim()) : []
  498 +
  499 + // 获取用户名列表
  500 + if (idsArray.length > 0) {
  501 + try {
  502 + const response = await getUserInfoList(idsArray)
  503 + if (response.code === 200 && response.data && response.data.list) {
  504 + node.approverNames = response.data.list.map(user => user.fullName || user.realName || user.id)
  505 + } else {
  506 + node.approverNames = idsArray
  507 + }
  508 + } catch (error) {
  509 + console.error('获取用户名失败:', error)
  510 + // 如果获取失败,使用ID作为名称
  511 + node.approverNames = idsArray
  512 + }
  513 + } else {
  514 + node.approverNames = []
  515 + }
  516 + },
  517 +
  518 + async loadPurchaseRecords() {
  519 + this.purchaseLoading = true
  520 + try {
  521 + const params = {
  522 + currentPage: this.purchaseQuery.currentPage,
  523 + pageSize: this.purchaseQuery.pageSize,
  524 + approveStatus: '未审批', // 只选择未审批的购买记录
  525 + createUserStoreId: this.dataForm.applicationStoreId || '暂无',
  526 + }
  527 + if (this.purchaseSearchKeyword) {
  528 + params.reimbursementCategoryName = this.purchaseSearchKeyword
  529 + }
  530 + const response = await request({
  531 + url: '/api/Extend/LqPurchaseRecords',
  532 + method: 'GET',
  533 + data: params
  534 + })
  535 + if (response.code === 200 && response.data) {
  536 + this.purchaseRecordList = response.data.list || []
  537 + this.purchaseTotal = (response.data.pagination && response.data.pagination.Total) || 0
  538 + }
  539 + } catch (error) {
  540 + console.error('加载购买记录失败:', error)
  541 + this.$message.error('加载购买记录失败')
  542 + } finally {
  543 + this.purchaseLoading = false
  544 + }
  545 + },
  546 +
  547 + handlePurchaseSearch() {
  548 + this.purchaseQuery.currentPage = 1
  549 + this.loadPurchaseRecords()
  550 + },
  551 +
  552 + handlePurchaseSelectionChange(selection) {
  553 + this.purchaseSelection = selection
  554 + },
  555 +
  556 + confirmPurchaseSelection() {
  557 + // 合并新选择的记录
  558 + const newRecords = this.purchaseSelection.filter(item => {
  559 + return !this.selectedPurchaseRecords.find(record => record.id === item.id)
  560 + })
  561 + this.selectedPurchaseRecords = [...this.selectedPurchaseRecords, ...newRecords]
  562 + this.calculateAmount()
  563 + this.showPurchaseDialog = false
  564 + },
  565 +
  566 + removePurchaseRecord(index) {
  567 + this.selectedPurchaseRecords.splice(index, 1)
  568 + this.calculateAmount()
  569 + },
  570 +
  571 + calculateAmount() {
  572 + // 计算总金额
  573 + let total = 0
  574 + this.selectedPurchaseRecords.forEach(record => {
  575 + total += Number(record.amount || 0)
  576 + })
  577 + this.dataForm.amount = total.toFixed(2)
  578 + this.dataForm.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(record => record.id)
  579 + },
  580 +
  581 + handlePurchaseSizeChange(val) {
  582 + this.purchaseQuery.pageSize = val
  583 + this.loadPurchaseRecords()
  584 + },
  585 +
  586 + handlePurchaseCurrentChange(val) {
  587 + this.purchaseQuery.currentPage = val
  588 + this.loadPurchaseRecords()
  589 + },
  590 +
  591 + validatePurchaseRecords(rule, value, callback) {
  592 + if (!this.selectedPurchaseRecords || this.selectedPurchaseRecords.length === 0) {
  593 + callback(new Error('请至少选择一条购买记录'))
  594 + } else {
  595 + callback()
  596 + }
  597 + },
  598 +
  599 + async handleSubmit() {
  600 + // 验证表单
  601 + this.$refs.form.validate(async (valid) => {
  602 + if (!valid) {
  603 + return false
  604 + }
  605 +
  606 + // 验证节点数量
  607 + if (this.dataForm.nodes.length < 1 || this.dataForm.nodes.length > 5) {
  608 + this.$message.warning('审批节点数量必须在1-5个之间')
  609 + return false
  610 + }
  611 +
  612 + // 验证每个节点
  613 + for (let i = 0; i < this.dataForm.nodes.length; i++) {
  614 + const node = this.dataForm.nodes[i]
  615 + if (!node.nodeName) {
  616 + this.$message.warning(`请填写节点${node.nodeOrder}的名称`)
  617 + return false
  618 + }
  619 + if (!node.approvalType) {
  620 + this.$message.warning(`请选择节点${node.nodeOrder}的审批类型`)
  621 + return false
  622 + }
  623 + // 验证审批人:approverIds可能是字符串(逗号分隔)或数组
  624 + let approverIdsArray = []
  625 + if (node.approverIds) {
  626 + if (Array.isArray(node.approverIds)) {
  627 + approverIdsArray = node.approverIds.filter(id => id)
  628 + } else if (typeof node.approverIds === 'string') {
  629 + approverIdsArray = node.approverIds.split(',').filter(id => id && id.trim())
  630 + }
  631 + }
  632 + if (approverIdsArray.length === 0) {
  633 + this.$message.warning(`请为节点${node.nodeOrder}选择至少一个审批人`)
  634 + return false
  635 + }
  636 + }
  637 +
  638 + // 验证购买记录
  639 + if (!this.selectedPurchaseRecords || this.selectedPurchaseRecords.length === 0) {
  640 + this.$message.warning('请至少选择一条购买记录')
  641 + return false
  642 + }
  643 +
  644 + this.submitting = true
  645 + try {
  646 + // 准备提交数据
  647 + const submitData = {
  648 + applicationUserId: this.dataForm.applicationUserId || undefined,
  649 + applicationUserName: this.dataForm.applicationUserName || undefined,
  650 + applicationStoreId: this.dataForm.applicationStoreId,
  651 + applicationTime: this.dataForm.applicationTime,
  652 + amount: this.dataForm.amount,
  653 + selectedPurchaseRecordIds: this.dataForm.selectedPurchaseRecordIds,
  654 + nodes: this.dataForm.nodes.map(node => {
  655 + // user-select组件返回的是字符串(逗号分隔),需要转换为数组
  656 + let approverIds = []
  657 + if (node.approverIds) {
  658 + if (Array.isArray(node.approverIds)) {
  659 + approverIds = node.approverIds.filter(id => id)
  660 + } else if (typeof node.approverIds === 'string') {
  661 + approverIds = node.approverIds.split(',').filter(id => id && id.trim())
  662 + }
  663 + }
  664 +
  665 + return {
  666 + nodeOrder: node.nodeOrder,
  667 + nodeName: node.nodeName,
  668 + approvalType: node.approvalType,
  669 + approverIds: approverIds,
  670 + approverNames: node.approverNames && node.approverNames.length > 0
  671 + ? node.approverNames
  672 + : approverIds // 如果没有名称,使用ID
  673 + }
  674 + })
  675 + }
  676 +
  677 + // 创建报销申请
  678 + const response = await request({
  679 + url: '/api/Extend/LqReimbursementApplication',
  680 + method: 'POST',
  681 + data: submitData
  682 + })
  683 +
  684 + if (response.code === 200) {
  685 + this.$message.success('创建成功')
  686 +
  687 + // 创建成功后,自动提交审批
  688 + let applicationId = null
  689 + if (response.data) {
  690 + // 如果返回的是对象,取id字段
  691 + if (typeof response.data === 'object' && response.data.id) {
  692 + applicationId = response.data.id
  693 + } else if (typeof response.data === 'string') {
  694 + // 如果返回的是字符串,直接使用
  695 + applicationId = response.data
  696 + } else {
  697 + // 如果返回的是其他类型,尝试转换为字符串
  698 + applicationId = String(response.data)
  699 + }
  700 + }
  701 +
  702 + if (applicationId) {
  703 + try {
  704 + await request({
  705 + url: `/api/Extend/LqReimbursementApplication/${applicationId}/Actions/SubmitApproval`,
  706 + method: 'POST'
  707 + })
  708 + this.$message.success('已自动提交审批')
  709 + } catch (error) {
  710 + console.error('提交审批失败:', error)
  711 + this.$message.warning('创建成功,但提交审批失败,请手动提交')
  712 + }
  713 + } else {
  714 + this.$message.warning('创建成功,但无法获取申请ID,请手动提交审批')
  715 + }
  716 +
  717 + this.visible = false
  718 + this.$emit('refresh')
  719 + } else {
  720 + this.$message.error(response.msg || '创建失败')
  721 + }
  722 + } catch (error) {
  723 + console.error('创建失败:', error)
  724 + this.$message.error('创建失败')
  725 + } finally {
  726 + this.submitting = false
  727 + }
  728 + })
  729 + },
  730 +
  731 + handleClose() {
  732 + this.visible = false
  733 + this.$refs.form && this.$refs.form.resetFields()
  734 + this.dataForm = {
  735 + applicationUserId: '',
  736 + applicationUserName: '',
  737 + applicationStoreId: '',
  738 + applicationTime: new Date().getTime(),
  739 + amount: '0.00',
  740 + selectedPurchaseRecordIds: [],
  741 + nodes: []
  742 + }
  743 + this.selectedPurchaseRecords = []
  744 + this.initNodes()
  745 + },
  746 +
  747 + formatMoney(amount) {
  748 + if (!amount) return '0.00'
  749 + return Number(amount).toLocaleString('zh-CN', {
  750 + minimumFractionDigits: 2,
  751 + maximumFractionDigits: 2
  752 + })
  753 + },
  754 +
  755 + formatDateTime(dateTime) {
  756 + if (!dateTime) return '无'
  757 + const date = new Date(dateTime)
  758 + const year = date.getFullYear()
  759 + const month = String(date.getMonth() + 1).padStart(2, '0')
  760 + const day = String(date.getDate()).padStart(2, '0')
  761 + const hours = String(date.getHours()).padStart(2, '0')
  762 + const minutes = String(date.getMinutes()).padStart(2, '0')
  763 + return `${year}-${month}-${day} ${hours}:${minutes}`
  764 + }
  765 + }
  766 +}
  767 +</script>
  768 +
  769 +<style lang="scss" scoped>
  770 +.form-content {
  771 + // max-height: 70vh;
  772 + overflow-y: auto;
  773 + padding: 0 10px;
  774 +}
  775 +
  776 +.form-card {
  777 + margin-bottom: 20px;
  778 +
  779 + .card-header {
  780 + display: flex;
  781 + align-items: center;
  782 + gap: 8px;
  783 + font-weight: 600;
  784 + color: #303133;
  785 +
  786 + i {
  787 + font-size: 18px;
  788 + color: #409EFF;
  789 + }
  790 +
  791 + .card-title {
  792 + flex: 1;
  793 + }
  794 + }
  795 +}
  796 +
  797 +.selected-count {
  798 + margin-left: 12px;
  799 + color: #67C23A;
  800 + font-size: 14px;
  801 +}
  802 +
  803 +.purchase-list {
  804 + margin-top: 16px;
  805 +}
  806 +
  807 +.nodes-list {
  808 + .node-item {
  809 + padding: 16px;
  810 + margin-bottom: 16px;
  811 + border: 1px solid #e4e7ed;
  812 + border-radius: 8px;
  813 + background: #fafafa;
  814 +
  815 + .node-header {
  816 + display: flex;
  817 + justify-content: space-between;
  818 + align-items: center;
  819 + margin-bottom: 16px;
  820 + padding-bottom: 12px;
  821 + border-bottom: 1px solid #e4e7ed;
  822 +
  823 + .node-order {
  824 + font-weight: 600;
  825 + color: #409EFF;
  826 + font-size: 16px;
  827 + }
  828 + }
  829 + }
  830 +
  831 + .empty-nodes {
  832 + padding: 40px;
  833 + text-align: center;
  834 + }
  835 +}
  836 +
  837 +.purchase-dialog-content {
  838 + .purchase-search {
  839 + margin-bottom: 16px;
  840 + }
  841 +
  842 + .purchase-pagination {
  843 + margin-top: 16px;
  844 + text-align: right;
  845 + }
  846 +}
  847 +</style>
  848 +
... ...
antis-ncc-admin/src/views/lqReimbursementApplication/index.vue
1 1 <template>
2 2 <div class="app-container">
3   - <!-- 页面头部 -->
4   - <div class="page-header">
5   - <div class="header-left">
6   - <h2>报销申请管理</h2>
7   - <p class="page-desc">管理报销申请,包括待办审批、所有申请列表等</p>
8   - </div>
9   - <div class="header-right">
10   - <el-button
11   - icon="el-icon-refresh"
12   - @click="getList"
13   - :loading="listLoading"
14   - >
15   - 刷新
16   - </el-button>
17   - </div>
18   - </div>
19   -
20 3 <!-- 标签页切换 -->
21 4 <div class="tabs-container">
22 5 <el-tabs v-model="activeTab" @tab-click="handleTabChange">
23   - <el-tab-pane label="所有待办" name="allPending">
  6 + <!-- <el-tab-pane label="所有待办" name="allPending">
24 7 <template slot="label">
25 8 <span><i class="el-icon-s-order"></i> 所有待办</span>
26 9 </template>
27   - </el-tab-pane>
  10 + </el-tab-pane> -->
28 11 <el-tab-pane label="我的待办" name="myPending">
29 12 <template slot="label">
30 13 <span><i class="el-icon-user"></i> 我的待办</span>
... ... @@ -40,17 +23,25 @@
40 23  
41 24 <!-- 搜索区域 -->
42 25 <div class="search-container">
43   - <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px" label-position="right">
44   - <el-form-item label="申请门店ID" prop="applicationStoreId">
45   - <el-input
  26 + <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="80px" label-position="right">
  27 + <el-form-item label="申请门店" prop="applicationStoreId">
  28 + <el-select
46 29 v-model="queryParams.applicationStoreId"
47   - placeholder="请输入申请门店ID"
  30 + placeholder="请选择申请门店"
48 31 clearable
  32 + filterable
49 33 style="width: 200px"
50   - />
  34 + >
  35 + <el-option
  36 + v-for="store in storeOptions"
  37 + :key="store.id"
  38 + :label="store.dm"
  39 + :value="store.id"
  40 + />
  41 + </el-select>
51 42 </el-form-item>
52 43 <template v-if="activeTab === 'all'">
53   - <el-form-item label="申请编号" prop="id">
  44 + <!-- <el-form-item label="申请编号" prop="id">
54 45 <el-input
55 46 v-model="queryParams.id"
56 47 placeholder="请输入申请编号"
... ... @@ -73,7 +64,7 @@
73 64 clearable
74 65 style="width: 200px"
75 66 />
76   - </el-form-item>
  67 + </el-form-item> -->
77 68 <el-form-item label="申请时间" prop="applicationTime">
78 69 <el-date-picker
79 70 v-model="queryParams.applicationTime"
... ... @@ -86,14 +77,14 @@
86 77 style="width: 240px"
87 78 />
88 79 </el-form-item>
89   - <el-form-item label="金额" prop="amount">
  80 + <!-- <el-form-item label="金额" prop="amount">
90 81 <el-input
91 82 v-model="queryParams.amount"
92 83 placeholder="请输入金额"
93 84 clearable
94 85 style="width: 200px"
95 86 />
96   - </el-form-item>
  87 + </el-form-item> -->
97 88 <el-form-item label="审批状态" prop="approveStatus">
98 89 <el-select
99 90 v-model="queryParams.approveStatus"
... ... @@ -108,18 +99,25 @@
108 99 <el-option label="已退回" value="已退回" />
109 100 </el-select>
110 101 </el-form-item>
111   - <el-form-item label="购买记录编号" prop="purchaseRecordsId">
  102 + <!-- <el-form-item label="购买记录编号" prop="purchaseRecordsId">
112 103 <el-input
113 104 v-model="queryParams.purchaseRecordsId"
114 105 placeholder="请输入购买记录编号"
115 106 clearable
116 107 style="width: 200px"
117 108 />
118   - </el-form-item>
  109 + </el-form-item> -->
119 110 </template>
120 111 <el-form-item>
121 112 <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
122 113 <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
  114 + <el-button
  115 + type="primary"
  116 + icon="el-icon-plus"
  117 + @click="handleAdd"
  118 + >
  119 + 新增
  120 + </el-button>
123 121 </el-form-item>
124 122 </el-form>
125 123 </div>
... ... @@ -131,21 +129,20 @@
131 129 :data="list"
132 130 border
133 131 stripe
134   - height="calc(100vh - 500px)"
135 132 :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
136 133 >
137 134 <!-- 申请编号 -->
138   - <el-table-column label="申请编号" width="180" align="center">
  135 + <!-- <el-table-column label="申请编号" >
139 136 <template slot-scope="scope">
140 137 <div class="id-item">
141 138 <i class="el-icon-document id-icon"></i>
142 139 <span>{{ scope.row.id || '无' }}</span>
143 140 </div>
144 141 </template>
145   - </el-table-column>
  142 + </el-table-column> -->
146 143  
147 144 <!-- 申请人 -->
148   - <el-table-column label="申请人" width="140" align="center">
  145 + <el-table-column label="申请人" >
149 146 <template slot-scope="scope">
150 147 <div class="user-item">
151 148 <i class="el-icon-user user-icon"></i>
... ... @@ -154,18 +151,18 @@
154 151 </template>
155 152 </el-table-column>
156 153  
157   - <!-- 申请门店ID -->
158   - <el-table-column label="申请门店ID" width="160" align="center">
  154 + <!-- 申请门店 -->
  155 + <el-table-column label="申请门店" >
159 156 <template slot-scope="scope">
160 157 <div class="store-item">
161 158 <i class="el-icon-shop store-icon"></i>
162   - <span>{{ scope.row.applicationStoreId || '无' }}</span>
  159 + <span>{{ scope.row.applicationStoreName || '无' }}</span>
163 160 </div>
164 161 </template>
165 162 </el-table-column>
166 163  
167 164 <!-- 申请时间 -->
168   - <el-table-column label="申请时间" width="180" align="center">
  165 + <el-table-column label="申请时间" >
169 166 <template slot-scope="scope">
170 167 <div class="time-item">
171 168 <i class="el-icon-time time-icon"></i>
... ... @@ -175,7 +172,7 @@
175 172 </el-table-column>
176 173  
177 174 <!-- 金额 -->
178   - <el-table-column label="金额" width="140" align="center">
  175 + <el-table-column label="金额" >
179 176 <template slot-scope="scope">
180 177 <div class="amount-item">
181 178 <i class="el-icon-money amount-icon"></i>
... ... @@ -185,7 +182,7 @@
185 182 </el-table-column>
186 183  
187 184 <!-- 审批状态 -->
188   - <el-table-column label="审批状态" width="120" align="center">
  185 + <el-table-column label="审批状态" >
189 186 <template slot-scope="scope">
190 187 <el-tag
191 188 :type="getStatusType(scope.row.approveStatus)"
... ... @@ -197,7 +194,7 @@
197 194 </el-table-column>
198 195  
199 196 <!-- 当前审批人 -->
200   - <el-table-column label="当前审批人" width="160" align="center" v-if="activeTab !== 'all'">
  197 + <el-table-column label="当前审批人" v-if="activeTab !== 'all'">
201 198 <template slot-scope="scope">
202 199 <div class="approver-item">
203 200 <i class="el-icon-user-solid approver-icon"></i>
... ... @@ -207,7 +204,7 @@
207 204 </el-table-column>
208 205  
209 206 <!-- 当前节点 -->
210   - <el-table-column label="当前节点" width="120" align="center" v-if="activeTab !== 'all'">
  207 + <el-table-column label="当前节点" v-if="activeTab !== 'all'">
211 208 <template slot-scope="scope">
212 209 <div class="node-item">
213 210 <i class="el-icon-s-order node-icon"></i>
... ... @@ -217,7 +214,7 @@
217 214 </el-table-column>
218 215  
219 216 <!-- 节点总数 -->
220   - <el-table-column label="节点总数" width="100" align="center" v-if="activeTab !== 'all'">
  217 + <el-table-column label="节点总数" v-if="activeTab !== 'all'">
221 218 <template slot-scope="scope">
222 219 <div class="node-count-item">
223 220 <i class="el-icon-s-data node-count-icon"></i>
... ... @@ -227,7 +224,7 @@
227 224 </el-table-column>
228 225  
229 226 <!-- 操作 -->
230   - <el-table-column label="操作" width="200" align="left" fixed="right">
  227 + <el-table-column label="操作" width="250" align="left" fixed="right">
231 228 <template slot-scope="scope">
232 229 <div class="action-buttons">
233 230 <el-button
... ... @@ -248,6 +245,16 @@
248 245 >
249 246 审批历史
250 247 </el-button>
  248 + <el-button
  249 + v-if="scope.row.approveStatus === '已退回'"
  250 + type="text"
  251 + icon="el-icon-edit"
  252 + @click="handleEdit(scope.row)"
  253 + size="small"
  254 + class="edit-btn"
  255 + >
  256 + 修改
  257 + </el-button>
251 258 </div>
252 259 </template>
253 260 </el-table-column>
... ... @@ -267,6 +274,13 @@
267 274 />
268 275 </div>
269 276  
  277 + <!-- 创建表单弹窗 -->
  278 + <form-dialog
  279 + v-if="formVisible"
  280 + ref="FormDialog"
  281 + @refresh="getList"
  282 + />
  283 +
270 284 <!-- 详情/审批弹窗 -->
271 285 <detail-dialog
272 286 v-if="detailVisible"
... ... @@ -279,28 +293,42 @@
279 293 v-if="historyVisible"
280 294 ref="ApprovalHistoryDialog"
281 295 />
  296 +
  297 + <!-- 编辑弹窗 -->
  298 + <edit-dialog
  299 + v-if="editVisible"
  300 + ref="EditDialog"
  301 + @refresh="getList"
  302 + />
282 303 </div>
283 304 </template>
284 305  
285 306 <script>
286 307 import request from '@/utils/request'
  308 +import FormDialog from './form-dialog.vue'
287 309 import DetailDialog from './detail-dialog.vue'
288 310 import ApprovalHistoryDialog from './approval-history-dialog.vue'
  311 +import EditDialog from './edit-dialog.vue'
289 312  
290 313 export default {
291 314 name: 'LqReimbursementApplication',
292 315 components: {
  316 + FormDialog,
293 317 DetailDialog,
294   - ApprovalHistoryDialog
  318 + ApprovalHistoryDialog,
  319 + EditDialog
295 320 },
296 321 data() {
297 322 return {
298   - activeTab: 'allPending', // 当前激活的标签页
  323 + activeTab: 'myPending', // 当前激活的标签页
299 324 listLoading: false,
300 325 list: [],
301 326 total: 0,
  327 + storeOptions: [], // 门店选项列表
  328 + formVisible: false,
302 329 detailVisible: false,
303 330 historyVisible: false,
  331 + editVisible: false,
304 332 queryParams: {
305 333 applicationStoreId: '',
306 334 id: '',
... ... @@ -320,9 +348,30 @@ export default {
320 348 }
321 349 },
322 350 created() {
  351 + this.loadStoreOptions()
323 352 this.getList()
324 353 },
325 354 methods: {
  355 + // 加载门店选项列表
  356 + async loadStoreOptions() {
  357 + try {
  358 + const response = await request({
  359 + url: '/api/Extend/LqMdxx',
  360 + method: 'GET',
  361 + data: {
  362 + currentPage: 1,
  363 + pageSize: 1000
  364 + }
  365 + })
  366 + if (response.code === 200 && response.data && response.data.list) {
  367 + this.storeOptions = response.data.list
  368 + }
  369 + } catch (error) {
  370 + console.error('加载门店列表失败:', error)
  371 + this.storeOptions = []
  372 + }
  373 + },
  374 +
326 375 // 获取列表数据
327 376 async getList() {
328 377 this.listLoading = true
... ... @@ -381,7 +430,7 @@ export default {
381 430  
382 431 if (response.code === 200) {
383 432 this.list = response.data.list || []
384   - this.total = (response.data.pagination && response.data.pagination.Total) || 0
  433 + this.total = response.data.pagination.total || 0
385 434 } else {
386 435 this.$message.error(response.msg || '获取列表失败')
387 436 this.list = []
... ... @@ -430,6 +479,14 @@ export default {
430 479 this.getList()
431 480 },
432 481  
  482 + // 新增
  483 + handleAdd() {
  484 + this.formVisible = true
  485 + this.$nextTick(() => {
  486 + this.$refs.FormDialog.init()
  487 + })
  488 + },
  489 +
433 490 // 查看详情/审批
434 491 handleView(row) {
435 492 this.detailVisible = true
... ... @@ -446,6 +503,14 @@ export default {
446 503 })
447 504 },
448 505  
  506 + // 编辑(修改已退回的申请)
  507 + handleEdit(row) {
  508 + this.editVisible = true
  509 + this.$nextTick(() => {
  510 + this.$refs.EditDialog.init(row.id)
  511 + })
  512 + },
  513 +
449 514 // 分页大小改变
450 515 handleSizeChange(val) {
451 516 this.listQuery.pageSize = val
... ... @@ -489,6 +554,13 @@ export default {
489 554 '已退回': 'warning'
490 555 }
491 556 return statusMap[status] || ''
  557 + },
  558 +
  559 + // 获取门店名称
  560 + getStoreName(storeId) {
  561 + if (!storeId) return '无'
  562 + const store = this.storeOptions.find(item => item.id === storeId)
  563 + return store ? store.dm : storeId
492 564 }
493 565 }
494 566 }
... ... @@ -565,8 +637,8 @@ export default {
565 637 .node-item,
566 638 .node-count-item {
567 639 display: flex;
568   - align-items: center;
569   - justify-content: center;
  640 + // align-items: center;
  641 + // justify-content: center;
570 642 gap: 6px;
571 643  
572 644 .id-icon {
... ... @@ -640,6 +712,14 @@ export default {
640 712 color: #606266;
641 713 }
642 714 }
  715 +
  716 + .edit-btn {
  717 + color: #409EFF;
  718 +
  719 + &:hover {
  720 + color: #66b1ff;
  721 + }
  722 + }
643 723 }
644 724  
645 725 .pagination-container {
... ...
antis-ncc-admin/src/views/wageManagement/detail-dialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="健康师工资详情"
  4 + :visible="visible"
  5 + width="1400px"
  6 + :close-on-click-modal="false"
  7 + @close="handleClose"
  8 + class="detail-dialog"
  9 + >
  10 + <div v-if="detailData" class="detail-content">
  11 + <!-- 基本信息 -->
  12 + <div class="detail-section">
  13 + <div class="section-title">
  14 + <i class="el-icon-user"></i>
  15 + <span>基本信息</span>
  16 + </div>
  17 + <div class="section-content">
  18 + <div class="info-row">
  19 + <div class="info-item">
  20 + <span class="label">门店名称:</span>
  21 + <span class="value">{{ detailData.StoreName || '无' }}</span>
  22 + </div>
  23 + <div class="info-item">
  24 + <span class="label">员工姓名:</span>
  25 + <span class="value">{{ detailData.EmployeeName || '无' }}</span>
  26 + </div>
  27 + <div class="info-item">
  28 + <span class="label">岗位:</span>
  29 + <span class="value">{{ detailData.Position || '无' }}</span>
  30 + </div>
  31 + <div class="info-item">
  32 + <span class="label">金三角战队:</span>
  33 + <span class="value">{{ detailData.GoldTriangleTeam || '无' }}</span>
  34 + </div>
  35 + <div class="info-item">
  36 + <span class="label">是否新店:</span>
  37 + <span class="value">{{ detailData.IsNewStore || '无' }}</span>
  38 + </div>
  39 + <div class="info-item">
  40 + <span class="label">新店保护阶段:</span>
  41 + <span class="value">{{ detailData.NewStoreProtectionStage || '0' }}</span>
  42 + </div>
  43 + <div class="info-item">
  44 + <span class="label">锁定状态:</span>
  45 + <el-tag :type="detailData.IsLocked === 1 ? 'danger' : 'success'" size="mini">
  46 + {{ detailData.IsLocked === 1 ? '已锁定' : '未锁定' }}
  47 + </el-tag>
  48 + </div>
  49 + </div>
  50 + </div>
  51 + </div>
  52 +
  53 + <!-- 业绩信息 -->
  54 + <div class="detail-section">
  55 + <div class="section-title">
  56 + <i class="el-icon-trophy"></i>
  57 + <span>业绩信息</span>
  58 + </div>
  59 + <div class="section-content">
  60 + <div class="info-row">
  61 + <div class="info-item">
  62 + <span class="label">总业绩:</span>
  63 + <span class="value highlight">{{ formatMoney(detailData.TotalPerformance) }}</span>
  64 + </div>
  65 + <div class="info-item">
  66 + <span class="label">基础业绩:</span>
  67 + <span class="value">{{ formatMoney(detailData.BasePerformance) }}</span>
  68 + </div>
  69 + <div class="info-item">
  70 + <span class="label">合作业绩:</span>
  71 + <span class="value">{{ formatMoney(detailData.CooperationPerformance) }}</span>
  72 + </div>
  73 + <div class="info-item">
  74 + <span class="label">奖励业绩:</span>
  75 + <span class="value">{{ formatMoney(detailData.RewardPerformance) }}</span>
  76 + </div>
  77 + <div class="info-item">
  78 + <span class="label">实际基础业绩:</span>
  79 + <span class="value">{{ formatMoney(detailData.ActualBasePerformance) }}</span>
  80 + </div>
  81 + <div class="info-item">
  82 + <span class="label">实际合作业绩:</span>
  83 + <span class="value">{{ formatMoney(detailData.ActualCooperationPerformance) }}</span>
  84 + </div>
  85 + <div class="info-item">
  86 + <span class="label">新客业绩:</span>
  87 + <span class="value">{{ formatMoney(detailData.NewCustomerPerformance) }}</span>
  88 + </div>
  89 + <div class="info-item">
  90 + <span class="label">升单业绩:</span>
  91 + <span class="value">{{ formatMoney(detailData.UpgradePerformance) }}</span>
  92 + </div>
  93 + <div class="info-item">
  94 + <span class="label">基础奖励业绩:</span>
  95 + <span class="value">{{ formatMoney(detailData.BaseRewardPerformance) }}</span>
  96 + </div>
  97 + <div class="info-item">
  98 + <span class="label">合作奖励业绩:</span>
  99 + <span class="value">{{ formatMoney(detailData.CooperationRewardPerformance) }}</span>
  100 + </div>
  101 + <div class="info-item">
  102 + <span class="label">其他业绩加:</span>
  103 + <span class="value">{{ formatMoney(detailData.OtherPerformanceAdd) }}</span>
  104 + </div>
  105 + <div class="info-item">
  106 + <span class="label">其他业绩减:</span>
  107 + <span class="value">{{ formatMoney(detailData.OtherPerformanceSubtract) }}</span>
  108 + </div>
  109 + <div class="info-item">
  110 + <span class="label">门店总业绩:</span>
  111 + <span class="value">{{ formatMoney(detailData.StoreTotalPerformance) }}</span>
  112 + </div>
  113 + <div class="info-item">
  114 + <span class="label">队伍业绩:</span>
  115 + <span class="value">{{ formatMoney(detailData.TeamPerformance) }}</span>
  116 + </div>
  117 + <div class="info-item">
  118 + <span class="label">占比:</span>
  119 + <span class="value">{{ formatPercent(detailData.Percentage) }}</span>
  120 + </div>
  121 + </div>
  122 + </div>
  123 + </div>
  124 +
  125 + <!-- 消耗数据 -->
  126 + <div class="detail-section">
  127 + <div class="section-title">
  128 + <i class="el-icon-data-analysis"></i>
  129 + <span>消耗数据</span>
  130 + </div>
  131 + <div class="section-content">
  132 + <div class="info-row">
  133 + <div class="info-item">
  134 + <span class="label">消耗:</span>
  135 + <span class="value">{{ formatMoney(detailData.Consumption) }}</span>
  136 + </div>
  137 + <div class="info-item">
  138 + <span class="label">项目数:</span>
  139 + <span class="value">{{ detailData.ProjectCount || '0' }}</span>
  140 + </div>
  141 + <div class="info-item">
  142 + <span class="label">到店人头:</span>
  143 + <span class="value">{{ detailData.CustomerCount || '0' }}</span>
  144 + </div>
  145 + <div class="info-item">
  146 + <span class="label">在店天数:</span>
  147 + <span class="value">{{ detailData.WorkingDays || '0' }}</span>
  148 + </div>
  149 + <div class="info-item">
  150 + <span class="label">请假天数:</span>
  151 + <span class="value">{{ detailData.LeaveDays || '0' }}</span>
  152 + </div>
  153 + <div class="info-item">
  154 + <span class="label">日均消耗:</span>
  155 + <span class="value">{{ formatMoney(detailData.DailyAverageConsumption) }}</span>
  156 + </div>
  157 + <div class="info-item">
  158 + <span class="label">日均项目数:</span>
  159 + <span class="value">{{ detailData.DailyAverageProjectCount || '0' }}</span>
  160 + </div>
  161 + <div class="info-item">
  162 + <span class="label">战队总消耗:</span>
  163 + <span class="value">{{ formatMoney(detailData.TeamTotalConsumption) }}</span>
  164 + </div>
  165 + </div>
  166 + </div>
  167 + </div>
  168 +
  169 + <!-- 新客数据 -->
  170 + <div class="detail-section">
  171 + <div class="section-title">
  172 + <i class="el-icon-user-solid"></i>
  173 + <span>新客数据</span>
  174 + </div>
  175 + <div class="section-content">
  176 + <div class="info-row">
  177 + <div class="info-item">
  178 + <span class="label">新客转化率:</span>
  179 + <span class="value">{{ formatPercent(detailData.NewCustomerConversionRate) }}</span>
  180 + </div>
  181 + <div class="info-item">
  182 + <span class="label">新客提点:</span>
  183 + <span class="value">{{ formatPercent(detailData.NewCustomerPoint) }}</span>
  184 + </div>
  185 + <div class="info-item">
  186 + <span class="label">新客业绩提成:</span>
  187 + <span class="value highlight">{{ formatMoney(detailData.NewCustomerCommission) }}</span>
  188 + </div>
  189 + </div>
  190 + </div>
  191 + </div>
  192 +
  193 + <!-- 升单数据 -->
  194 + <div class="detail-section">
  195 + <div class="section-title">
  196 + <i class="el-icon-arrow-up"></i>
  197 + <span>升单数据</span>
  198 + </div>
  199 + <div class="section-content">
  200 + <div class="info-row">
  201 + <div class="info-item">
  202 + <span class="label">升单人头数:</span>
  203 + <span class="value">{{ detailData.UpgradeCustomerCount || '0' }}</span>
  204 + </div>
  205 + <div class="info-item">
  206 + <span class="label">升单提点:</span>
  207 + <span class="value">{{ formatPercent(detailData.UpgradePoint) }}</span>
  208 + </div>
  209 + <div class="info-item">
  210 + <span class="label">升单业绩提成:</span>
  211 + <span class="value highlight">{{ formatMoney(detailData.UpgradeCommission) }}</span>
  212 + </div>
  213 + </div>
  214 + </div>
  215 + </div>
  216 +
  217 + <!-- 提成信息 -->
  218 + <div class="detail-section">
  219 + <div class="section-title">
  220 + <i class="el-icon-money"></i>
  221 + <span>提成信息</span>
  222 + </div>
  223 + <div class="section-content">
  224 + <div class="info-row">
  225 + <div class="info-item">
  226 + <span class="label">提点:</span>
  227 + <span class="value">{{ formatPercent(detailData.CommissionPoint) }}</span>
  228 + </div>
  229 + <div class="info-item">
  230 + <span class="label">基础业绩提成:</span>
  231 + <span class="value">{{ formatMoney(detailData.BasePerformanceCommission) }}</span>
  232 + </div>
  233 + <div class="info-item">
  234 + <span class="label">合作业绩提成:</span>
  235 + <span class="value">{{ formatMoney(detailData.CooperationPerformanceCommission) }}</span>
  236 + </div>
  237 + <div class="info-item">
  238 + <span class="label">顾问提成:</span>
  239 + <span class="value">{{ formatMoney(detailData.ConsultantCommission) }}</span>
  240 + </div>
  241 + <div class="info-item">
  242 + <span class="label">门店T区提成:</span>
  243 + <span class="value">{{ formatMoney(detailData.StoreTZoneCommission) }}</span>
  244 + </div>
  245 + <div class="info-item">
  246 + <span class="label">提成合计:</span>
  247 + <span class="value highlight">{{ formatMoney(detailData.TotalCommission) }}</span>
  248 + </div>
  249 + </div>
  250 + </div>
  251 + </div>
  252 +
  253 + <!-- 工资信息 -->
  254 + <div class="detail-section">
  255 + <div class="section-title">
  256 + <i class="el-icon-wallet"></i>
  257 + <span>工资信息</span>
  258 + </div>
  259 + <div class="section-content">
  260 + <div class="info-row">
  261 + <div class="info-item">
  262 + <span class="label">健康师底薪:</span>
  263 + <span class="value">{{ formatMoney(detailData.HealthCoachBaseSalary) }}</span>
  264 + </div>
  265 + <div class="info-item">
  266 + <span class="label">手工费:</span>
  267 + <span class="value">{{ formatMoney(detailData.HandworkFee) }}</span>
  268 + </div>
  269 + <div class="info-item">
  270 + <span class="label">额外手工费:</span>
  271 + <span class="value">{{ formatMoney(detailData.OutherHandworkFee) }}</span>
  272 + </div>
  273 + <div class="info-item">
  274 + <span class="label">核算应发工资:</span>
  275 + <span class="value">{{ formatMoney(detailData.CalculatedGrossSalary) }}</span>
  276 + </div>
  277 + <div class="info-item">
  278 + <span class="label">保底工资:</span>
  279 + <span class="value">{{ formatMoney(detailData.GuaranteedSalary) }}</span>
  280 + </div>
  281 + <div class="info-item">
  282 + <span class="label">保底请假扣款:</span>
  283 + <span class="value">{{ formatMoney(detailData.GuaranteedLeaveDeduction) }}</span>
  284 + </div>
  285 + <div class="info-item">
  286 + <span class="label">保底底薪:</span>
  287 + <span class="value">{{ formatMoney(detailData.GuaranteedBaseSalary) }}</span>
  288 + </div>
  289 + <div class="info-item">
  290 + <span class="label">保底补差:</span>
  291 + <span class="value">{{ formatMoney(detailData.GuaranteedSupplement) }}</span>
  292 + </div>
  293 + <div class="info-item">
  294 + <span class="label">最终应发工资:</span>
  295 + <span class="value highlight">{{ formatMoney(detailData.FinalGrossSalary) }}</span>
  296 + </div>
  297 + </div>
  298 + </div>
  299 + </div>
  300 +
  301 + <!-- 补贴信息 -->
  302 + <div class="detail-section">
  303 + <div class="section-title">
  304 + <i class="el-icon-circle-plus"></i>
  305 + <span>补贴信息</span>
  306 + </div>
  307 + <div class="section-content">
  308 + <div class="info-row">
  309 + <div class="info-item">
  310 + <span class="label">车补:</span>
  311 + <span class="value">{{ formatMoney(detailData.TransportationAllowance) }}</span>
  312 + </div>
  313 + <div class="info-item">
  314 + <span class="label">少休费:</span>
  315 + <span class="value">{{ formatMoney(detailData.LessRest) }}</span>
  316 + </div>
  317 + <div class="info-item">
  318 + <span class="label">全勤奖:</span>
  319 + <span class="value">{{ formatMoney(detailData.FullAttendance) }}</span>
  320 + </div>
  321 + <div class="info-item">
  322 + <span class="label">当月培训补贴:</span>
  323 + <span class="value">{{ formatMoney(detailData.MonthlyTrainingSubsidy) }}</span>
  324 + </div>
  325 + <div class="info-item">
  326 + <span class="label">当月交通补贴:</span>
  327 + <span class="value">{{ formatMoney(detailData.MonthlyTransportSubsidy) }}</span>
  328 + </div>
  329 + <div class="info-item">
  330 + <span class="label">上月培训补贴:</span>
  331 + <span class="value">{{ formatMoney(detailData.LastMonthTrainingSubsidy) }}</span>
  332 + </div>
  333 + <div class="info-item">
  334 + <span class="label">上月交通补贴:</span>
  335 + <span class="value">{{ formatMoney(detailData.LastMonthTransportSubsidy) }}</span>
  336 + </div>
  337 + <div class="info-item">
  338 + <span class="label">补贴合计:</span>
  339 + <span class="value highlight">{{ formatMoney(detailData.TotalSubsidy) }}</span>
  340 + </div>
  341 + </div>
  342 + </div>
  343 + </div>
  344 +
  345 + <!-- 扣款信息 -->
  346 + <div class="detail-section">
  347 + <div class="section-title">
  348 + <i class="el-icon-circle-minus"></i>
  349 + <span>扣款信息</span>
  350 + </div>
  351 + <div class="section-content">
  352 + <div class="info-row">
  353 + <div class="info-item">
  354 + <span class="label">缺卡扣款:</span>
  355 + <span class="value">{{ formatMoney(detailData.MissingCard) }}</span>
  356 + </div>
  357 + <div class="info-item">
  358 + <span class="label">迟到扣款:</span>
  359 + <span class="value">{{ formatMoney(detailData.LateArrival) }}</span>
  360 + </div>
  361 + <div class="info-item">
  362 + <span class="label">请假扣款:</span>
  363 + <span class="value">{{ formatMoney(detailData.LeaveDeduction) }}</span>
  364 + </div>
  365 + <div class="info-item">
  366 + <span class="label">扣社保:</span>
  367 + <span class="value">{{ formatMoney(detailData.SocialInsuranceDeduction) }}</span>
  368 + </div>
  369 + <div class="info-item">
  370 + <span class="label">扣除奖励:</span>
  371 + <span class="value">{{ formatMoney(detailData.RewardDeduction) }}</span>
  372 + </div>
  373 + <div class="info-item">
  374 + <span class="label">扣住宿费:</span>
  375 + <span class="value">{{ formatMoney(detailData.AccommodationDeduction) }}</span>
  376 + </div>
  377 + <div class="info-item">
  378 + <span class="label">扣学习期费用:</span>
  379 + <span class="value">{{ formatMoney(detailData.StudyPeriodDeduction) }}</span>
  380 + </div>
  381 + <div class="info-item">
  382 + <span class="label">扣工作服费用:</span>
  383 + <span class="value">{{ formatMoney(detailData.WorkClothesDeduction) }}</span>
  384 + </div>
  385 + <div class="info-item">
  386 + <span class="label">扣款合计:</span>
  387 + <span class="value highlight">{{ formatMoney(detailData.TotalDeduction) }}</span>
  388 + </div>
  389 + </div>
  390 + </div>
  391 + </div>
  392 +
  393 + <!-- 其他信息 -->
  394 + <div class="detail-section">
  395 + <div class="section-title">
  396 + <i class="el-icon-document"></i>
  397 + <span>其他信息</span>
  398 + </div>
  399 + <div class="section-content">
  400 + <div class="info-row">
  401 + <div class="info-item">
  402 + <span class="label">发奖金:</span>
  403 + <span class="value">{{ formatMoney(detailData.Bonus) }}</span>
  404 + </div>
  405 + <div class="info-item">
  406 + <span class="label">退手机押金:</span>
  407 + <span class="value">{{ formatMoney(detailData.ReturnPhoneDeposit) }}</span>
  408 + </div>
  409 + <div class="info-item">
  410 + <span class="label">退住宿押金:</span>
  411 + <span class="value">{{ formatMoney(detailData.ReturnAccommodationDeposit) }}</span>
  412 + </div>
  413 + <div class="info-item">
  414 + <span class="label">补发上月:</span>
  415 + <span class="value">{{ formatMoney(detailData.LastMonthSupplement) }}</span>
  416 + </div>
  417 + </div>
  418 + </div>
  419 + </div>
  420 +
  421 + <!-- 支付信息 -->
  422 + <div class="detail-section">
  423 + <div class="section-title">
  424 + <i class="el-icon-credit-card"></i>
  425 + <span>支付信息</span>
  426 + </div>
  427 + <div class="section-content">
  428 + <div class="info-row">
  429 + <div class="info-item">
  430 + <span class="label">实发工资:</span>
  431 + <span class="value highlight large">{{ formatMoney(detailData.ActualSalary) }}</span>
  432 + </div>
  433 + <div class="info-item">
  434 + <span class="label">当月是否发放:</span>
  435 + <span class="value">{{ detailData.MonthlyPaymentStatus || '无' }}</span>
  436 + </div>
  437 + <div class="info-item">
  438 + <span class="label">支付金额:</span>
  439 + <span class="value">{{ formatMoney(detailData.PaidAmount) }}</span>
  440 + </div>
  441 + <div class="info-item">
  442 + <span class="label">待支付金额:</span>
  443 + <span class="value">{{ formatMoney(detailData.PendingAmount) }}</span>
  444 + </div>
  445 + <div class="info-item">
  446 + <span class="label">当月支付总额:</span>
  447 + <span class="value">{{ formatMoney(detailData.MonthlyTotalPayment) }}</span>
  448 + </div>
  449 + </div>
  450 + </div>
  451 + </div>
  452 + </div>
  453 +
  454 + <div v-else class="no-data">
  455 + <i class="el-icon-warning"></i>
  456 + <span>暂无数据</span>
  457 + </div>
  458 + </el-dialog>
  459 +</template>
  460 +
  461 +<script>
  462 +export default {
  463 + name: 'HealthCoachDetailDialog',
  464 + props: {
  465 + visible: {
  466 + type: Boolean,
  467 + default: false
  468 + },
  469 + detailData: {
  470 + type: Object,
  471 + default: null
  472 + }
  473 + },
  474 + methods: {
  475 + handleClose() {
  476 + this.$emit('update:visible', false)
  477 + this.$emit('close')
  478 + },
  479 + formatMoney(value) {
  480 + if (value === null || value === undefined || value === '') {
  481 + return '0.00'
  482 + }
  483 + return Number(value).toFixed(2)
  484 + },
  485 + formatPercent(value) {
  486 + if (value === null || value === undefined || value === '') {
  487 + return '0.00%'
  488 + }
  489 + return (Number(value) * 100).toFixed(2) + '%'
  490 + }
  491 + }
  492 +}
  493 +</script>
  494 +
  495 +<style lang="scss" scoped>
  496 +.detail-dialog {
  497 + .detail-content {
  498 + max-height: 70vh;
  499 + overflow-y: auto;
  500 + padding-right: 10px;
  501 + }
  502 +
  503 + .detail-section {
  504 + margin-bottom: 20px;
  505 + background: #fff;
  506 + border-radius: 8px;
  507 + padding: 20px;
  508 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  509 +
  510 + .section-title {
  511 + display: flex;
  512 + align-items: center;
  513 + gap: 8px;
  514 + margin-bottom: 16px;
  515 + padding-bottom: 12px;
  516 + border-bottom: 2px solid #409EFF;
  517 + font-size: 16px;
  518 + font-weight: 600;
  519 + color: #303133;
  520 +
  521 + i {
  522 + color: #409EFF;
  523 + font-size: 18px;
  524 + }
  525 + }
  526 +
  527 + .section-content {
  528 + .info-row {
  529 + display: flex;
  530 + flex-wrap: wrap;
  531 + gap: 20px 40px;
  532 +
  533 + .info-item {
  534 + display: flex;
  535 + align-items: center;
  536 + min-width: 200px;
  537 + flex: 0 0 calc(33.333% - 27px);
  538 +
  539 + .label {
  540 + color: #606266;
  541 + font-size: 14px;
  542 + white-space: nowrap;
  543 + margin-right: 8px;
  544 + }
  545 +
  546 + .value {
  547 + color: #303133;
  548 + font-size: 14px;
  549 + font-weight: 500;
  550 +
  551 + &.highlight {
  552 + color: #409EFF;
  553 + font-weight: 600;
  554 + }
  555 +
  556 + &.large {
  557 + font-size: 18px;
  558 + color: #67C23A;
  559 + }
  560 + }
  561 + }
  562 + }
  563 + }
  564 + }
  565 +
  566 + .no-data {
  567 + display: flex;
  568 + flex-direction: column;
  569 + align-items: center;
  570 + justify-content: center;
  571 + padding: 60px 20px;
  572 + color: #909399;
  573 +
  574 + i {
  575 + font-size: 48px;
  576 + margin-bottom: 16px;
  577 + }
  578 +
  579 + span {
  580 + font-size: 14px;
  581 + }
  582 + }
  583 +
  584 + .dialog-footer {
  585 + text-align: right;
  586 + }
  587 +}
  588 +
  589 +// 滚动条样式
  590 +.detail-content::-webkit-scrollbar {
  591 + width: 6px;
  592 +}
  593 +
  594 +.detail-content::-webkit-scrollbar-track {
  595 + background: #f1f1f1;
  596 + border-radius: 3px;
  597 +}
  598 +
  599 +.detail-content::-webkit-scrollbar-thumb {
  600 + background: #c1c1c1;
  601 + border-radius: 3px;
  602 +
  603 + &:hover {
  604 + background: #a8a8a8;
  605 + }
  606 +}
  607 +</style>
  608 +
... ...
antis-ncc-admin/src/views/wageManagement/healthCoach.vue
... ... @@ -523,6 +523,18 @@
523 523 </el-tag>
524 524 </template>
525 525 </el-table-column>
  526 + <el-table-column label="操作" width="100" align="center" fixed="right">
  527 + <template slot-scope="scope">
  528 + <el-button
  529 + type="primary"
  530 + size="mini"
  531 + icon="el-icon-view"
  532 + @click="handleViewDetail(scope.row)"
  533 + >
  534 + 详情
  535 + </el-button>
  536 + </template>
  537 + </el-table-column>
526 538 </NCC-table>
527 539 </div>
528 540  
... ... @@ -576,6 +588,14 @@
576 588 @close="handleExtraDataDialogClose"
577 589 @refresh="getList"
578 590 />
  591 +
  592 + <!-- 详情弹窗 -->
  593 + <detail-dialog
  594 + :visible="detailDialogVisible"
  595 + :detail-data="currentDetailData"
  596 + @update:visible="detailDialogVisible = $event"
  597 + @close="handleDetailDialogClose"
  598 + />
579 599 </div>
580 600 </template>
581 601  
... ... @@ -583,11 +603,13 @@
583 603 import { getHealthCoachSalaryList, calculateHealthCoachSalary } from '@/api/extend/healthCoachSalary'
584 604 import { getStoreSelector } from '@/api/extend/store'
585 605 import ExtraDataDialog from './extra-data-dialog.vue'
  606 +import DetailDialog from './detail-dialog.vue'
586 607  
587 608 export default {
588 609 name: 'HealthCoachSalary',
589 610 components: {
590   - ExtraDataDialog
  611 + ExtraDataDialog,
  612 + DetailDialog
591 613 },
592 614 data() {
593 615 // 获取当前年月
... ... @@ -612,6 +634,8 @@ export default {
612 634 calculateYear: getCurrentYear(),
613 635 calculateMonth: getCurrentMonth(),
614 636 extraDataDialogVisible: false,
  637 + detailDialogVisible: false,
  638 + currentDetailData: null,
615 639 queryParams: {
616 640 currentPage: 1,
617 641 pageSize: 300, // 写死为300
... ... @@ -745,6 +769,18 @@ export default {
745 769 this.extraDataDialogVisible = false
746 770 },
747 771  
  772 + // 查看详情
  773 + handleViewDetail(row) {
  774 + this.currentDetailData = row
  775 + this.detailDialogVisible = true
  776 + },
  777 +
  778 + // 关闭详情弹窗
  779 + handleDetailDialogClose() {
  780 + this.detailDialogVisible = false
  781 + this.currentDetailData = null
  782 + },
  783 +
748 784 // 确认计算工资
749 785 async handleCalculateConfirm() {
750 786 if (!this.calculateYear || !this.calculateMonth) {
... ...
绿纤uni-app/apis/modules/oauth.js
... ... @@ -23,4 +23,8 @@ export default {
23 23 UploadBase64Image(data) {
24 24 return request.post(`${config.getApiBaseUrl()}/api/file/UploadBase64Image`, data);
25 25 },
  26 + // 获取用户列表
  27 + getUserList(params) {
  28 + return request.get(`${config.getApiBaseUrl()}/api/permission/Users`, params);
  29 + },
26 30 }
27 31 \ No newline at end of file
... ...
绿纤uni-app/apis/modules/reimbursement.js
... ... @@ -26,13 +26,30 @@ export default {
26 26 getStoreList(params) {
27 27 return request.get(`${config.getApiBaseUrl()}/api/Extend/LqMdxx`, params);
28 28 },
29   - // 通过审批
30   - approveReimbursement(id) {
31   - return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReimbursementApplication/${id}/Actions/Approve`);
  29 + // 审批(支持通过、不通过、退回)
  30 + approveReimbursement(id, params) {
  31 + const queryParams = new URLSearchParams()
  32 + if (params && params.result) {
  33 + queryParams.append('result', params.result)
  34 + }
  35 + if (params && params.opinion) {
  36 + queryParams.append('opinion', params.opinion)
  37 + }
  38 + const queryString = queryParams.toString()
  39 + const url = `${config.getApiBaseUrl()}/api/Extend/LqReimbursementApplication/${id}/Actions/Approve${queryString ? '?' + queryString : ''}`
  40 + return request.post(url);
32 41 },
33 42 // 拒绝审批
34 43 rejectReimbursement(id) {
35 44 return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReimbursementApplication/${id}/Actions/Reject`);
  45 + },
  46 + // 提交审批
  47 + submitApproval(id) {
  48 + return request.post(`${config.getApiBaseUrl()}/api/Extend/LqReimbursementApplication/${id}/Actions/SubmitApproval`);
  49 + },
  50 + // 获取我的待办列表
  51 + getMyPendingList(params) {
  52 + return request.get(`${config.getApiBaseUrl()}/api/Extend/LqReimbursementApplication/Actions/PendingApproval`, params);
36 53 }
37 54 }
38 55  
... ...
绿纤uni-app/pages/purchase-list/purchase-list.vue
... ... @@ -79,7 +79,7 @@
79 79  
80 80 <text class="btn-text">查看</text>
81 81 </view>
82   - <view v-if="item.approveStatus === '未审批' || item.approveStatus === '未通过'" class="action-btn edit-btn" @click.stop="handleEdit(item)">
  82 + <view v-if="item.approveStatus === '未审批' || item.approveStatus === '已退回'" class="action-btn edit-btn" @click.stop="handleEdit(item)">
83 83  
84 84 <text class="btn-text">编辑</text>
85 85 </view>
... ... @@ -306,7 +306,7 @@
306 306  
307 307 // 编辑记录(只有未审批状态才能编辑)
308 308 handleEdit(item) {
309   - if (item.approveStatus === '未审批' || item.approveStatus === '未通过') {
  309 + if (item.approveStatus === '未审批' || item.approveStatus === '已退回') {
310 310 uni.navigateTo({
311 311 url: `/pages/purchase-form/purchase-form?id=${item.id}`
312 312 })
... ...
绿纤uni-app/pages/reimbursement-audit-list/reimbursement-audit-list copy.vue 0 → 100644
  1 +<template>
  2 + <view class="container">
  3 + <!-- 日期范围筛选 -->
  4 + <view class="date-filter-card">
  5 + <view class="date-filter-header">
  6 + <view class="date-range-display" @click="showCalendarFun">
  7 + <text class="date-text">{{ formatDateRange() }}</text>
  8 + <text class="calendar-icon">📅</text>
  9 + </view>
  10 + </view>
  11 + <view class="status-filter">
  12 + <text class="status-filter-label">审批状态</text>
  13 + <picker mode="selector" :range="approveStatusOptions" :range-key="'label'" :value="approveStatusIndex" @change="handleStatusChange">
  14 + <view class="status-picker">
  15 + <text class="status-text">{{ approveStatusOptions[approveStatusIndex].label }}</text>
  16 + <text class="picker-arrow">▼</text>
  17 + </view>
  18 + </picker>
  19 + </view>
  20 + </view>
  21 +
  22 + <!-- 日历组件 -->
  23 + <uni-calendar
  24 + :range="true"
  25 + ref="calendar"
  26 + @confirm="handleDateConfirm"
  27 + :insert="false"
  28 + ></uni-calendar>
  29 +
  30 + <!-- 数据列表 -->
  31 + <view class="list-card">
  32 + <view class="list-header">
  33 + <text class="header-text">报销审核列表</text>
  34 + <text class="total-count">共 {{ totalCount }} 条</text>
  35 + </view>
  36 +
  37 + <scroll-view scroll-y class="list-content" @scrolltolower="loadMore">
  38 + <view v-for="(item, index) in dataList" :key="index" class="list-item">
  39 + <view class="item-header">
  40 + <view class="applicant-name">{{ item.applicationUserName || '无' }}</view>
  41 + <view class="application-time">{{ formatTime(item.applicationTime) }}</view>
  42 + </view>
  43 + <view class="item-details">
  44 + <view class="detail-item">
  45 + <text class="detail-label">门店:</text>
  46 + <text class="detail-value">{{ getStoreName(item.applicationStoreId) || '无' }}</text>
  47 + </view>
  48 + <view class="detail-item">
  49 + <text class="detail-label">总金额:</text>
  50 + <text class="detail-value amount">¥{{ item.amount || 0 }}</text>
  51 + </view>
  52 + </view>
  53 + <view class="item-footer">
  54 + <view class="status-badge" :class="item.approveStatus=='已审批'?'approved':item.approveStatus=='待审批'?'pending':item.approveStatus=='未通过'?'rejected':'unapproved'">
  55 + {{ item.approveStatus || '未审批' }}
  56 + </view>
  57 + <view class="action-buttons">
  58 + <view v-if="item.approveStatus != '未审批'" class="action-btn view-btn" @click.stop="handleView(item)">
  59 + <text class="btn-text">查看</text>
  60 + </view>
  61 + <view v-if="item.approveStatus === '未审批'" class="action-btn audit-btn" @click.stop="handleAudit(item)">
  62 + <text class="btn-text">审核</text>
  63 + </view>
  64 + </view>
  65 + </view>
  66 + </view>
  67 +
  68 + <!-- 空状态 -->
  69 + <view v-if="!loading && dataList.length === 0" class="empty-state">
  70 + <view class="empty-icon">📋</view>
  71 + <view>暂无报销申请记录</view>
  72 + </view>
  73 +
  74 + <!-- 加载状态 -->
  75 + <view v-if="loading && dataList.length === 0" class="loading">正在加载数据...</view>
  76 +
  77 + <!-- 加载更多 -->
  78 + <u-loadmore v-if="dataList.length > 0" :status="loadmoreStatus" :load-text="loadText"></u-loadmore>
  79 + </scroll-view>
  80 + </view>
  81 + </view>
  82 +</template>
  83 +
  84 +<script>
  85 + import api from '@/apis/index.js'
  86 + import uniCalendar from '@/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue'
  87 +
  88 + export default {
  89 + components: {
  90 + uniCalendar
  91 + },
  92 + data() {
  93 + // 初始化日期范围(本月1号到今天)
  94 + const now = new Date()
  95 + const year = now.getFullYear()
  96 + const month = now.getMonth()
  97 + const firstDay = new Date(year, month, 1)
  98 + const today = new Date(year, now.getMonth(), now.getDate(), 23, 59, 59, 999)
  99 +
  100 + // 格式化日期为 YYYY-MM-DD(使用本地时间,避免时区问题)
  101 + const formatDateLocal = (date) => {
  102 + const y = date.getFullYear()
  103 + const m = String(date.getMonth() + 1).padStart(2, '0')
  104 + const d = String(date.getDate()).padStart(2, '0')
  105 + return `${y}-${m}-${d}`
  106 + }
  107 +
  108 + return {
  109 + loading: false,
  110 + dataList: [],
  111 + currentPage: 1,
  112 + pageSize: 10,
  113 + totalCount: 0,
  114 + hasMore: true,
  115 + loadmoreStatus: 'loadmore',
  116 + loadText: {
  117 + loadmore: '点击或上拉加载更多',
  118 + loading: '正在加载...',
  119 + nomore: '没有更多了'
  120 + },
  121 + userInfo: uni.getStorageSync('userInfo'),
  122 + newuserInfo: uni.getStorageSync('newuserInfo'),
  123 + storeOptions: [],
  124 + // 日期范围筛选相关
  125 + showCalendar: false,
  126 + startDate: formatDateLocal(firstDay),
  127 + endDate: formatDateLocal(now),
  128 + dateRange: [firstDay.getTime(), today.getTime()],
  129 + // 审批状态筛选
  130 + approveStatusIndex: 1, // 默认选择"未审批"(索引1)
  131 + approveStatusOptions: [
  132 + { label: '全部', value: '' },
  133 + { label: '未审批', value: '未审批' },
  134 + { label: '已审批', value: '已审批' },
  135 + { label: '未通过', value: '未通过' }
  136 + ]
  137 + }
  138 + },
  139 +
  140 + onLoad() {
  141 + this.loadStoreOptions()
  142 + this.loadReimbursementList()
  143 + },
  144 +
  145 + onShow() {
  146 + // 从详情页返回时刷新列表
  147 + this.refreshData()
  148 + },
  149 +
  150 + methods: {
  151 + // 加载门店列表
  152 + async loadStoreOptions() {
  153 + try {
  154 + const res = await api.getStoreList({
  155 + currentPage: 1,
  156 + pageSize: 1000
  157 + })
  158 + if (res.code === 200 && res.data && res.data.list) {
  159 + this.storeOptions = res.data.list
  160 + } else {
  161 + this.storeOptions = []
  162 + }
  163 + } catch (error) {
  164 + console.error('加载门店列表失败:', error)
  165 + this.storeOptions = []
  166 + }
  167 + },
  168 +
  169 + // 获取门店名称
  170 + getStoreName(storeId) {
  171 + if (!storeId) return '无'
  172 + const store = this.storeOptions.find(item => item.id === storeId)
  173 + return store ? store.dm : '无'
  174 + },
  175 +
  176 + // 显示日历
  177 + showCalendarFun() {
  178 + this.$refs.calendar.open()
  179 + },
  180 +
  181 + // 日期确认
  182 + handleDateConfirm(e) {
  183 + console.log('日期选择结果:', e)
  184 +
  185 + // uni-calendar组件返回的是包含start和end的对象
  186 + if (e && e.range && e.range.data) {
  187 + // 更新dateRange用于API调用(时间戳格式)
  188 + const startTime = new Date(e.range.data[0]).getTime()
  189 + const endTime = new Date(e.range.data[e.range.data.length-1]).getTime()
  190 + this.dateRange = [startTime, endTime]
  191 +
  192 + // 更新startDate和endDate用于显示
  193 + this.startDate = e.range.data[0]
  194 + this.endDate = e.range.data[e.range.data.length-1]
  195 +
  196 + // 日期变化时重新加载数据
  197 + this.currentPage = 1
  198 + this.loadReimbursementList()
  199 + }
  200 + },
  201 +
  202 + // 格式化日期范围显示
  203 + formatDateRange() {
  204 + if (!this.startDate || !this.endDate) {
  205 + return '请选择日期'
  206 + }
  207 +
  208 + // 处理日期字符串,确保正确解析
  209 + const startDate = new Date(this.startDate + 'T00:00:00')
  210 + const endDate = new Date(this.endDate + 'T00:00:00')
  211 +
  212 + const formatDate = (date) => {
  213 + const year = date.getFullYear()
  214 + const month = String(date.getMonth() + 1).padStart(2, '0')
  215 + const day = String(date.getDate()).padStart(2, '0')
  216 + return `${year}/${month}/${day}`
  217 + }
  218 +
  219 + return `${formatDate(startDate)} - ${formatDate(endDate)}`
  220 + },
  221 +
  222 + // 审批状态变化
  223 + handleStatusChange(e) {
  224 + this.approveStatusIndex = e.detail.value
  225 + this.currentPage = 1
  226 + this.loadReimbursementList()
  227 + },
  228 +
  229 + // 加载报销申请列表
  230 + async loadReimbursementList() {
  231 + if (this.loading) return
  232 +
  233 + try {
  234 + this.loading = true
  235 + this.loadmoreStatus = 'loading'
  236 +
  237 + // 构建查询参数
  238 + const params = {
  239 + currentPage: this.currentPage,
  240 + pageSize: this.pageSize
  241 + }
  242 +
  243 + // 添加审批状态筛选
  244 + const selectedStatus = this.approveStatusOptions[this.approveStatusIndex]
  245 + if (selectedStatus && selectedStatus.value) {
  246 + params.approveStatus = selectedStatus.value
  247 + }
  248 +
  249 + // 添加日期范围筛选
  250 + if (this.startDate && this.endDate) {
  251 + const startTimestamp = new Date(this.startDate + 'T00:00:00').getTime()
  252 + const endTimestamp = new Date(this.endDate + 'T23:59:59').getTime()
  253 + params.applicationTime = `${startTimestamp},${endTimestamp}`
  254 + }
  255 +
  256 + const res = await api.getReimbursementList(params)
  257 +
  258 + if (res.code === 200 && res.data) {
  259 + const list = res.data.list || []
  260 + const pagination = res.data.pagination || {}
  261 +
  262 + if (this.currentPage === 1) {
  263 + this.dataList = list
  264 + } else {
  265 + this.dataList = [...this.dataList, ...list]
  266 + }
  267 +
  268 + this.totalCount = pagination.total || 0
  269 + this.hasMore = this.dataList.length < this.totalCount
  270 +
  271 + if (!this.hasMore) {
  272 + this.loadmoreStatus = 'nomore'
  273 + } else {
  274 + this.loadmoreStatus = 'loadmore'
  275 + }
  276 + } else {
  277 + uni.showToast({
  278 + title: res.message || '加载失败',
  279 + icon: 'none'
  280 + })
  281 + }
  282 + } catch (error) {
  283 + console.error('加载报销审核列表失败:', error)
  284 + uni.showToast({
  285 + title: '加载失败,请重试',
  286 + icon: 'none'
  287 + })
  288 + } finally {
  289 + this.loading = false
  290 + }
  291 + },
  292 +
  293 + // 刷新数据
  294 + refreshData() {
  295 + this.currentPage = 1
  296 + this.hasMore = true
  297 + this.loadmoreStatus = 'loadmore'
  298 + this.dataList = []
  299 + this.loadReimbursementList()
  300 + },
  301 +
  302 + // 加载更多
  303 + async loadMore() {
  304 + if (this.loading || !this.hasMore || this.loadmoreStatus === 'nomore') return
  305 +
  306 + this.loadmoreStatus = 'loading'
  307 + this.currentPage++
  308 + await this.loadReimbursementList()
  309 + },
  310 +
  311 + // 查看详情
  312 + handleView(item) {
  313 + uni.navigateTo({
  314 + url: `/pages/reimbursement-audit-detail/reimbursement-audit-detail?id=${item.id}`
  315 + })
  316 + },
  317 +
  318 + // 审核(跳转到详情页进行审核)
  319 + handleAudit(item) {
  320 + uni.navigateTo({
  321 + url: `/pages/reimbursement-audit-detail/reimbursement-audit-detail?id=${item.id}`
  322 + })
  323 + },
  324 +
  325 + // 格式化时间
  326 + formatTime(timestamp) {
  327 + if (!timestamp) return '无'
  328 + const date = new Date(timestamp)
  329 + return date.toLocaleString('zh-CN', {
  330 + year: 'numeric',
  331 + month: '2-digit',
  332 + day: '2-digit',
  333 + hour: '2-digit',
  334 + minute: '2-digit'
  335 + })
  336 + }
  337 + }
  338 + }
  339 +</script>
  340 +
  341 +<style lang="scss" scoped>
  342 + .container {
  343 + min-height: 100vh;
  344 + background: linear-gradient(135deg, #e8f5e9 0%, #b2dfdb 100%);
  345 + padding: 40rpx;
  346 + box-sizing: border-box;
  347 + }
  348 +
  349 + .date-filter-card {
  350 + background: #fff;
  351 + border-radius: 24rpx;
  352 + padding: 32rpx;
  353 + margin-bottom: 24rpx;
  354 + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.1);
  355 + }
  356 +
  357 + .date-filter-header {
  358 + margin-bottom: 24rpx;
  359 + }
  360 +
  361 + .date-range-display {
  362 + display: flex;
  363 + align-items: center;
  364 + justify-content: space-between;
  365 + padding: 24rpx;
  366 + background: #f9fff9;
  367 + border: 3rpx solid #c8e6c9;
  368 + border-radius: 16rpx;
  369 + }
  370 +
  371 + .date-text {
  372 + font-size: 28rpx;
  373 + color: #2e7d32;
  374 + font-weight: 500;
  375 + }
  376 +
  377 + .calendar-icon {
  378 + font-size: 32rpx;
  379 + }
  380 +
  381 + .status-filter {
  382 + display: flex;
  383 + align-items: center;
  384 + gap: 24rpx;
  385 + justify-content: space-between;
  386 + }
  387 +
  388 + .status-filter-label {
  389 + font-size: 28rpx;
  390 + color: #2e7d32;
  391 + font-weight: 500;
  392 + }
  393 +
  394 + .status-picker {
  395 + flex: 1;
  396 + display: flex;
  397 + align-items: center;
  398 + justify-content: space-between;
  399 + padding: 24rpx;
  400 + background: #f9fff9;
  401 + border: 3rpx solid #c8e6c9;
  402 + border-radius: 16rpx;
  403 + }
  404 +
  405 + .status-text {
  406 + font-size: 28rpx;
  407 + color: #2e7d32;
  408 + }
  409 +
  410 + .picker-arrow {
  411 + font-size: 24rpx;
  412 + color: #6a9c6a;
  413 + margin-left: 10rpx;
  414 + }
  415 +
  416 + .list-card {
  417 + background: #fff;
  418 + border-radius: 24rpx;
  419 + overflow: hidden;
  420 + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.1);
  421 + }
  422 +
  423 + .list-header {
  424 + display: flex;
  425 + justify-content: space-between;
  426 + align-items: center;
  427 + padding: 32rpx;
  428 + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%);
  429 + }
  430 +
  431 + .header-text {
  432 + font-size: 32rpx;
  433 + color: #fff;
  434 + font-weight: 600;
  435 + letter-spacing: 2rpx;
  436 + }
  437 +
  438 + .total-count {
  439 + font-size: 24rpx;
  440 + color: #fff;
  441 + opacity: 0.9;
  442 + }
  443 +
  444 + .list-content {
  445 + max-height: calc(100vh - 400rpx);
  446 + }
  447 +
  448 + .list-item {
  449 + padding: 32rpx;
  450 + border-bottom: 2rpx solid #f0f0f0;
  451 + transition: all 0.2s ease;
  452 + }
  453 +
  454 + .list-item:last-child {
  455 + border-bottom: none;
  456 + }
  457 +
  458 + .list-item:active {
  459 + background: #f9fff9;
  460 + }
  461 +
  462 + .item-header {
  463 + display: flex;
  464 + justify-content: space-between;
  465 + align-items: center;
  466 + margin-bottom: 16rpx;
  467 + }
  468 +
  469 + .applicant-name {
  470 + font-size: 32rpx;
  471 + color: #2e7d32;
  472 + font-weight: 600;
  473 + }
  474 +
  475 + .application-time {
  476 + font-size: 24rpx;
  477 + color: #6a9c6a;
  478 + }
  479 +
  480 + .item-details {
  481 + display: flex;
  482 + flex-wrap: wrap;
  483 + gap: 24rpx;
  484 + margin-bottom: 16rpx;
  485 + }
  486 +
  487 + .detail-item {
  488 + display: flex;
  489 + align-items: center;
  490 + gap: 8rpx;
  491 + }
  492 +
  493 + .detail-label {
  494 + font-size: 24rpx;
  495 + color: #6a9c6a;
  496 + }
  497 +
  498 + .detail-value {
  499 + font-size: 28rpx;
  500 + color: #2e7d32;
  501 + font-weight: 500;
  502 + }
  503 +
  504 + .detail-value.amount {
  505 + color: #f57c00;
  506 + font-weight: 600;
  507 + font-size: 32rpx;
  508 + }
  509 +
  510 + .item-footer {
  511 + display: flex;
  512 + justify-content: space-between;
  513 + align-items: center;
  514 + margin-top: 16rpx;
  515 + }
  516 +
  517 + .status-badge {
  518 + display: inline-block;
  519 + padding: 8rpx 24rpx;
  520 + border-radius: 40rpx;
  521 + font-size: 24rpx;
  522 + font-weight: 500;
  523 + text-align: center;
  524 + }
  525 +
  526 + .status-badge.unapproved {
  527 + background: #fff3e0;
  528 + color: #ef6c00;
  529 + border: 2rpx solid #ffe0b2;
  530 + }
  531 +
  532 + .status-badge.pending {
  533 + background: #e3f2fd;
  534 + color: #1976d2;
  535 + border: 2rpx solid #bbdefb;
  536 + }
  537 +
  538 + .status-badge.approved {
  539 + background: #e8f5e9;
  540 + color: #2e7d32;
  541 + border: 2rpx solid #c8e6c9;
  542 + }
  543 +
  544 + .status-badge.rejected {
  545 + background: #ffebee;
  546 + color: #c62828;
  547 + border: 2rpx solid #ffcdd2;
  548 + }
  549 +
  550 + .action-buttons {
  551 + display: flex;
  552 + align-items: center;
  553 + gap: 16rpx;
  554 + }
  555 +
  556 + .action-btn {
  557 + display: flex;
  558 + align-items: center;
  559 + padding: 12rpx 24rpx;
  560 + border-radius: 24rpx;
  561 + font-size: 24rpx;
  562 + font-weight: 500;
  563 + transition: all 0.2s ease;
  564 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  565 + }
  566 +
  567 + .action-btn:active {
  568 + transform: scale(0.95);
  569 + box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.15);
  570 + }
  571 +
  572 + .view-btn {
  573 + background: linear-gradient(135deg, #42a5f5 0%, #1e88e5 100%);
  574 + color: #fff;
  575 + }
  576 +
  577 + .view-btn:active {
  578 + background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
  579 + }
  580 +
  581 + .audit-btn {
  582 + background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
  583 + color: #fff;
  584 + }
  585 +
  586 + .audit-btn:active {
  587 + background: linear-gradient(135deg, #f57c00 0%, #e65100 100%);
  588 + }
  589 +
  590 + .btn-text {
  591 + letter-spacing: 1rpx;
  592 + }
  593 +
  594 + .loading {
  595 + text-align: center;
  596 + padding: 80rpx 40rpx;
  597 + color: #6a9c6a;
  598 + font-size: 28rpx;
  599 + }
  600 +
  601 + .empty-state {
  602 + text-align: center;
  603 + padding: 120rpx 40rpx;
  604 + color: #6a9c6a;
  605 + }
  606 +
  607 + .empty-icon {
  608 + font-size: 120rpx;
  609 + margin-bottom: 32rpx;
  610 + opacity: 0.5;
  611 + }
  612 +</style>
  613 +
... ...
绿纤uni-app/pages/reimbursement-audit-list/reimbursement-audit-list.vue
1 1 <template>
2 2 <view class="container">
3 3 <!-- 日期范围筛选 -->
4   - <view class="date-filter-card">
  4 + <!-- <view class="date-filter-card">
5 5 <view class="date-filter-header">
6 6 <view class="date-range-display" @click="showCalendarFun">
7 7 <text class="date-text">{{ formatDateRange() }}</text>
... ... @@ -17,7 +17,7 @@
17 17 </view>
18 18 </picker>
19 19 </view>
20   - </view>
  20 + </view> -->
21 21  
22 22 <!-- 日历组件 -->
23 23 <uni-calendar
... ... @@ -51,16 +51,16 @@
51 51 </view>
52 52 </view>
53 53 <view class="item-footer">
54   - <view class="status-badge" :class="item.approveStatus=='已审批'?'approved':item.approveStatus=='待审批'?'pending':item.approveStatus=='未通过'?'rejected':'unapproved'">
55   - {{ item.approveStatus || '未审批' }}
  54 + <view class="status-badge" :class="getStatusClass(item.approveStatus)">
  55 + {{ item.approveStatus || '待审批' }}
56 56 </view>
57 57 <view class="action-buttons">
58   - <view v-if="item.approveStatus != '未审批'" class="action-btn view-btn" @click.stop="handleView(item)">
59   - <text class="btn-text">查看</text>
60   - </view>
61   - <view v-if="item.approveStatus === '未审批'" class="action-btn audit-btn" @click.stop="handleAudit(item)">
  58 + <view v-if="item.approveStatus === '待审批' || item.approveStatus === '审批中'" class="action-btn audit-btn" @click.stop="handleAudit(item)">
62 59 <text class="btn-text">审核</text>
63 60 </view>
  61 + <view v-else class="action-btn view-btn" @click.stop="handleView(item)">
  62 + <text class="btn-text">查看</text>
  63 + </view>
64 64 </view>
65 65 </view>
66 66 </view>
... ... @@ -127,12 +127,14 @@
127 127 endDate: formatDateLocal(now),
128 128 dateRange: [firstDay.getTime(), today.getTime()],
129 129 // 审批状态筛选
130   - approveStatusIndex: 1, // 默认选择"未审批"(索引1
  130 + approveStatusIndex: 0, // 默认选择"全部"(索引0
131 131 approveStatusOptions: [
132 132 { label: '全部', value: '' },
133   - { label: '未审批', value: '未审批' },
134   - { label: '已审批', value: '已审批' },
135   - { label: '未通过', value: '未通过' }
  133 + { label: '待审批', value: '待审批' },
  134 + { label: '审批中', value: '审批中' },
  135 + { label: '已通过', value: '已通过' },
  136 + { label: '未通过', value: '未通过' },
  137 + { label: '已退回', value: '已退回' }
136 138 ]
137 139 }
138 140 },
... ... @@ -226,7 +228,7 @@
226 228 this.loadReimbursementList()
227 229 },
228 230  
229   - // 加载报销申请列表
  231 + // 加载报销申请列表(我的待办)
230 232 async loadReimbursementList() {
231 233 if (this.loading) return
232 234  
... ... @@ -234,39 +236,73 @@
234 236 this.loading = true
235 237 this.loadmoreStatus = 'loading'
236 238  
237   - // 构建查询参数
  239 + // 构建查询参数(我的待办接口只支持门店筛选和分页参数)
238 240 const params = {
239 241 currentPage: this.currentPage,
240   - pageSize: this.pageSize
  242 + pageSize: this.pageSize,
  243 + sidx: 'applicationTime',
  244 + sort: 'desc'
241 245 }
242 246  
243   - // 添加审批状态筛选
244   - const selectedStatus = this.approveStatusOptions[this.approveStatusIndex]
245   - if (selectedStatus && selectedStatus.value) {
246   - params.approveStatus = selectedStatus.value
247   - }
248   -
249   - // 添加日期范围筛选
250   - if (this.startDate && this.endDate) {
251   - const startTimestamp = new Date(this.startDate + 'T00:00:00').getTime()
252   - const endTimestamp = new Date(this.endDate + 'T23:59:59').getTime()
253   - params.applicationTime = `${startTimestamp},${endTimestamp}`
254   - }
  247 + // 我的待办接口支持门店筛选(如果需要的话,可以添加门店选择功能)
  248 + // 注意:我的待办接口不支持审批状态和日期范围筛选
  249 + // 如果需要这些筛选,需要在前端进行过滤,或者使用其他接口
255 250  
256   - const res = await api.getReimbursementList(params)
  251 + // 使用我的待办接口
  252 + const res = await api.getMyPendingList(params)
257 253  
258 254 if (res.code === 200 && res.data) {
259   - const list = res.data.list || []
  255 + let list = res.data.list || []
260 256 const pagination = res.data.pagination || {}
261 257  
  258 + // 前端过滤:根据审批状态筛选
  259 + const selectedStatus = this.approveStatusOptions[this.approveStatusIndex]
  260 + if (selectedStatus && selectedStatus.value) {
  261 + list = list.filter(item => item.approveStatus === selectedStatus.value)
  262 + }
  263 +
  264 + // 前端过滤:根据日期范围筛选
  265 + if (this.startDate && this.endDate) {
  266 + const startTimestamp = new Date(this.startDate + 'T00:00:00').getTime()
  267 + const endTimestamp = new Date(this.endDate + 'T23:59:59').getTime()
  268 + list = list.filter(item => {
  269 + if (!item.applicationTime) return false
  270 + const itemTime = new Date(item.applicationTime).getTime()
  271 + return itemTime >= startTimestamp && itemTime <= endTimestamp
  272 + })
  273 + }
  274 +
262 275 if (this.currentPage === 1) {
263 276 this.dataList = list
264 277 } else {
265   - this.dataList = [...this.dataList, ...list]
  278 + // 加载更多时,需要去重(避免重复数据)
  279 + const existingIds = new Set(this.dataList.map(item => item.id))
  280 + const newItems = list.filter(item => !existingIds.has(item.id))
  281 + this.dataList = [...this.dataList, ...newItems]
266 282 }
267 283  
268   - this.totalCount = pagination.total || 0
269   - this.hasMore = this.dataList.length < this.totalCount
  284 + // 注意:由于前端过滤,总数可能不准确
  285 + // 如果第一页且有筛选条件,使用过滤后的数量;否则使用后端返回的总数
  286 + if (this.currentPage === 1) {
  287 + const hasFilter = (selectedStatus && selectedStatus.value) || (this.startDate && this.endDate)
  288 + if (hasFilter) {
  289 + // 有筛选条件时,总数不准确,使用当前列表长度
  290 + this.totalCount = list.length
  291 + } else {
  292 + // 无筛选条件时,使用后端返回的总数
  293 + this.totalCount = pagination.total || list.length
  294 + }
  295 + }
  296 +
  297 + // 判断是否还有更多数据
  298 + const hasFilter = (selectedStatus && selectedStatus.value) || (this.startDate && this.endDate)
  299 + if (hasFilter) {
  300 + // 有筛选条件时,如果返回的数据少于pageSize,说明没有更多了
  301 + this.hasMore = list.length >= this.pageSize
  302 + } else {
  303 + // 无筛选条件时,使用后端分页信息
  304 + this.hasMore = this.dataList.length < (pagination.total || 0)
  305 + }
270 306  
271 307 if (!this.hasMore) {
272 308 this.loadmoreStatus = 'nomore'
... ... @@ -311,14 +347,14 @@
311 347 // 查看详情
312 348 handleView(item) {
313 349 uni.navigateTo({
314   - url: `/pages/reimbursement-audit-detail/reimbursement-audit-detail?id=${item.id}`
  350 + url: `/pages/reimbursement-detail/reimbursement-detail?id=${item.id}`
315 351 })
316 352 },
317 353  
318 354 // 审核(跳转到详情页进行审核)
319 355 handleAudit(item) {
320 356 uni.navigateTo({
321   - url: `/pages/reimbursement-audit-detail/reimbursement-audit-detail?id=${item.id}`
  357 + url: `/pages/reimbursement-detail/reimbursement-detail?id=${item.id}`
322 358 })
323 359 },
324 360  
... ... @@ -333,6 +369,24 @@
333 369 hour: '2-digit',
334 370 minute: '2-digit'
335 371 })
  372 + },
  373 +
  374 + // 获取状态样式类
  375 + getStatusClass(status) {
  376 + switch (status) {
  377 + case '待审批':
  378 + return 'pending'
  379 + case '审批中':
  380 + return 'pending'
  381 + case '已通过':
  382 + return 'approved'
  383 + case '未通过':
  384 + return 'rejected'
  385 + case '已退回':
  386 + return 'rejected'
  387 + default:
  388 + return 'unapproved'
  389 + }
336 390 }
337 391 }
338 392 }
... ...
绿纤uni-app/pages/reimbursement-detail/reimbursement-detail - 副本.vue 0 → 100644
  1 +<template>
  2 + <view class="container">
  3 + <!-- 详情卡片 -->
  4 + <view class="detail-card">
  5 + <view class="card-header">报销申请详情</view>
  6 + <view class="detail-content">
  7 + <!-- 加载状态 -->
  8 + <view v-if="loading" class="loading">正在加载详情...</view>
  9 +
  10 + <!-- 错误状态 -->
  11 + <view v-else-if="error" class="error-state">
  12 + <view>{{ error }}</view>
  13 + <u-button class="retry-btn" @click="retryLoad" type="primary" size="small">重试</u-button>
  14 + </view>
  15 +
  16 + <!-- 详情内容 -->
  17 + <view v-else-if="detailData" class="info-sections">
  18 + <!-- 基本信息 -->
  19 + <view class="info-section">
  20 + <view class="section-title">基本信息</view>
  21 + <view class="info-grid">
  22 + <view class="info-item">
  23 + <text class="info-label">申请人</text>
  24 + <text class="info-value">{{ detailData.applicationUserName || '无' }}</text>
  25 + </view>
  26 + <view class="info-item">
  27 + <text class="info-label">申请门店</text>
  28 + <text class="info-value">{{ getStoreName(detailData.applicationStoreId) || '无' }}</text>
  29 + </view>
  30 + <view class="info-item">
  31 + <text class="info-label">申请时间</text>
  32 + <text class="info-value">{{ formatTime(detailData.applicationTime) }}</text>
  33 + </view>
  34 + <view class="info-item">
  35 + <text class="info-label">总金额</text>
  36 + <text class="info-value amount">¥{{ detailData.amount || 0 }}</text>
  37 + </view>
  38 + <view class="info-item">
  39 + <text class="info-label">审批状态</text>
  40 + <view class="status-badge" :class="detailData.approveStatus=='已审批'?'approved':detailData.approveStatus=='待审批'?'pending':detailData.approveStatus=='未通过'?'rejected':'unapproved'">
  41 + {{ detailData.approveStatus || '未审批' }}
  42 + </view>
  43 + </view>
  44 + </view>
  45 + </view>
  46 +
  47 + <!-- 购买物品清单 -->
  48 + <view class="info-section" v-if="purchaseRecords && purchaseRecords.length > 0">
  49 + <view class="section-title">购买物品清单</view>
  50 + <view class="purchase-records-list">
  51 + <view v-for="(item, index) in purchaseRecords" :key="item.id || index" class="purchase-record-item">
  52 + <view class="record-header">
  53 + <text class="record-category">{{ item.reimbursementCategoryName || '无' }}</text>
  54 + <text class="record-amount">¥{{ item.amount || 0 }}</text>
  55 + </view>
  56 + <view class="record-details">
  57 + <view class="record-detail-item">
  58 + <text class="record-label">单价:</text>
  59 + <text class="record-value">¥{{ item.unitPrice || 0 }}</text>
  60 + </view>
  61 + <view class="record-detail-item">
  62 + <text class="record-label">数量:</text>
  63 + <text class="record-value">{{ item.quantity || 0 }}</text>
  64 + </view>
  65 + <view class="record-detail-item">
  66 + <text class="record-label">购买时间:</text>
  67 + <text class="record-value">{{ formatTime(item.purchaseTime) }}</text>
  68 + </view>
  69 + </view>
  70 + <view class="record-memo" v-if="item.memo">
  71 + <text class="record-label">备注:</text>
  72 + <text class="record-value">{{ item.memo }}</text>
  73 + </view>
  74 + <!-- 附件信息 -->
  75 + <view class="record-attachment" v-if="item.attachment && item.attachment.length > 0">
  76 + <text class="record-label">附件:</text>
  77 + <view class="attachment-list">
  78 + <view
  79 + v-for="(file, fileIndex) in item.attachment"
  80 + :key="fileIndex"
  81 + class="attachment-item"
  82 + @click="previewFile(file, item.attachment)">
  83 + <text class="file-icon">📎</text>
  84 + <text class="file-name">{{ file.name || '文件' }}</text>
  85 + </view>
  86 + </view>
  87 + </view>
  88 + </view>
  89 + </view>
  90 + </view>
  91 +
  92 + <!-- 审批信息 -->
  93 + <view class="info-section" v-if="detailData.approveStatus && detailData.approveStatus !== '未审批'">
  94 + <view class="section-title">审批信息</view>
  95 + <view class="info-grid">
  96 + <view class="info-item" v-if="detailData.approveUser">
  97 + <text class="info-label">审批人</text>
  98 + <text class="info-value">{{ detailData.approveUserName || detailData.approveUser || '无' }}</text>
  99 + </view>
  100 + <view class="info-item" v-if="detailData.approveTime">
  101 + <text class="info-label">审批时间</text>
  102 + <text class="info-value">{{ formatTime(detailData.approveTime) }}</text>
  103 + </view>
  104 + </view>
  105 + </view>
  106 + </view>
  107 + </view>
  108 + </view>
  109 + </view>
  110 +</template>
  111 +
  112 +<script>
  113 + import api from '@/apis/index.js'
  114 + import purchaseApi from '@/apis/modules/purchase.js'
  115 + import config from '@/common/config.js'
  116 +
  117 + export default {
  118 + data() {
  119 + return {
  120 + loading: false,
  121 + error: null,
  122 + detailData: null,
  123 + purchaseRecords: [],
  124 + reimbursementId: null,
  125 + storeOptions: [],
  126 + baseUrl: config.getApiBaseUrl()
  127 + }
  128 + },
  129 +
  130 + onLoad(options) {
  131 + if (options.id) {
  132 + this.reimbursementId = options.id
  133 + this.loadStoreOptions()
  134 + this.loadDetail()
  135 + } else {
  136 + this.error = '缺少记录ID'
  137 + }
  138 + },
  139 +
  140 + methods: {
  141 + // 加载门店列表
  142 + async loadStoreOptions() {
  143 + try {
  144 + const res = await api.getStoreList({
  145 + currentPage: 1,
  146 + pageSize: 1000
  147 + })
  148 + if (res.code === 200 && res.data && res.data.list) {
  149 + this.storeOptions = res.data.list
  150 + } else {
  151 + this.storeOptions = []
  152 + }
  153 + } catch (error) {
  154 + console.error('加载门店列表失败:', error)
  155 + this.storeOptions = []
  156 + }
  157 + },
  158 +
  159 + // 获取门店名称
  160 + getStoreName(storeId) {
  161 + if (!storeId) return '无'
  162 + const store = this.storeOptions.find(item => item.id === storeId)
  163 + return store ? store.dm : '无'
  164 + },
  165 +
  166 + // 加载详情数据
  167 + async loadDetail() {
  168 + if (!this.reimbursementId) return
  169 +
  170 + try {
  171 + this.loading = true
  172 + this.error = null
  173 +
  174 + const res = await api.getReimbursementDetail(this.reimbursementId)
  175 +
  176 + if (res.code === 200 && res.data) {
  177 + this.detailData = res.data
  178 +
  179 + // 加载关联的购买记录
  180 + if (this.detailData.purchaseRecordsId) {
  181 + await this.loadPurchaseRecords(this.detailData.purchaseRecordsId)
  182 + }
  183 + } else {
  184 + this.error = res.message || '加载详情失败'
  185 + }
  186 + } catch (error) {
  187 + console.error('加载详情失败:', error)
  188 + this.error = '加载详情失败,请重试'
  189 + } finally {
  190 + this.loading = false
  191 + }
  192 + },
  193 +
  194 + // 加载购买记录
  195 + async loadPurchaseRecords(purchaseRecordsId) {
  196 + if (!purchaseRecordsId) {
  197 + this.purchaseRecords = []
  198 + return
  199 + }
  200 +
  201 + // 处理逗号分隔的ID字符串
  202 + const normalizedStr = purchaseRecordsId.toString().replace(/[\r\n]+/g, ',').replace(/\s+/g, '')
  203 + const ids = normalizedStr.split(',').map(id => id.trim()).filter(id => id && id !== '')
  204 +
  205 + if (ids.length === 0) {
  206 + this.purchaseRecords = []
  207 + return
  208 + }
  209 +
  210 + try {
  211 + // 批量加载购买记录
  212 + const promises = ids.map(id => {
  213 + return purchaseApi.getPurchaseDetail(id)
  214 + .then(res => res.code === 200 ? res.data : null)
  215 + .catch(() => null)
  216 + })
  217 +
  218 + const results = await Promise.all(promises)
  219 + this.purchaseRecords = results.filter(item => item !== null)
  220 + } catch (error) {
  221 + console.error('加载购买记录失败:', error)
  222 + this.purchaseRecords = []
  223 + }
  224 + },
  225 +
  226 + // 重试加载
  227 + retryLoad() {
  228 + this.loadDetail()
  229 + },
  230 +
  231 + // 格式化时间
  232 + formatTime(timestamp) {
  233 + if (!timestamp) return '无'
  234 + const date = new Date(timestamp)
  235 + return date.toLocaleString('zh-CN', {
  236 + year: 'numeric',
  237 + month: '2-digit',
  238 + day: '2-digit',
  239 + hour: '2-digit',
  240 + minute: '2-digit'
  241 + })
  242 + },
  243 +
  244 + // 获取状态样式类
  245 + getStatusClass(status) {
  246 + switch (status) {
  247 + case '已审批':
  248 + return 'approved'
  249 + case '待审批':
  250 + return 'pending'
  251 + case '未通过':
  252 + return 'rejected'
  253 + case '未审批':
  254 + default:
  255 + return 'unapproved'
  256 + }
  257 + },
  258 + // 预览文件
  259 + previewFile(file, attachmentList) {
  260 + if (!file || !file.url) {
  261 + uni.showToast({
  262 + title: '附件URL不存在',
  263 + icon: 'none'
  264 + })
  265 + return
  266 + }
  267 +
  268 + const fullUrl = file.url.startsWith('http') ? file.url : `${this.baseUrl}${file.url}`
  269 +
  270 + // 判断文件类型
  271 + const fileExtension = this.getFileExtension(file.name || file.url)
  272 + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
  273 +
  274 + if (imageExtensions.includes(fileExtension.toLowerCase())) {
  275 + // 图片类型:预览
  276 + // 构建所有图片URL列表
  277 + const imageUrls = attachmentList
  278 + .filter(item => {
  279 + const ext = this.getFileExtension(item.name || item.url)
  280 + return imageExtensions.includes(ext.toLowerCase())
  281 + })
  282 + .map(item => {
  283 + const url = item.url.startsWith('http') ? item.url : `${this.baseUrl}${item.url}`
  284 + return url
  285 + })
  286 +
  287 + const currentIndex = imageUrls.findIndex(url => url === fullUrl)
  288 +
  289 + uni.previewImage({
  290 + urls: imageUrls,
  291 + current: currentIndex >= 0 ? currentIndex : 0
  292 + })
  293 + } else {
  294 + // 其他类型:下载或打开
  295 + uni.showToast({
  296 + title: '非图片文件,请下载查看',
  297 + icon: 'none'
  298 + })
  299 + // 可以在这里添加下载逻辑
  300 + // uni.downloadFile({
  301 + // url: fullUrl,
  302 + // success: (res) => {
  303 + // // 处理下载成功
  304 + // }
  305 + // })
  306 + }
  307 + },
  308 + // 获取文件扩展名
  309 + getFileExtension(fileName) {
  310 + if (!fileName) return ''
  311 + const lastDot = fileName.lastIndexOf('.')
  312 + return lastDot > -1 ? fileName.substring(lastDot + 1) : ''
  313 + }
  314 + }
  315 + }
  316 +</script>
  317 +
  318 +<style lang="scss" scoped>
  319 + .container {
  320 + min-height: 100vh;
  321 + background: linear-gradient(135deg, #e8f5e9 0%, #b2dfdb 100%);
  322 + padding: 40rpx;
  323 + box-sizing: border-box;
  324 + }
  325 +
  326 + .detail-card {
  327 + background: #fff;
  328 + border-radius: 32rpx;
  329 + box-shadow: 0 8rpx 32rpx rgba(76, 175, 80, 0.1);
  330 + overflow: hidden;
  331 + }
  332 +
  333 + .card-header {
  334 + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%);
  335 + padding: 32rpx 40rpx;
  336 + color: #fff;
  337 + font-size: 36rpx;
  338 + font-weight: 600;
  339 + letter-spacing: 2rpx;
  340 + text-align: center;
  341 + }
  342 +
  343 + .detail-content {
  344 + padding: 40rpx;
  345 + }
  346 +
  347 + .loading {
  348 + text-align: center;
  349 + padding: 80rpx 40rpx;
  350 + color: #6a9c6a;
  351 + font-size: 28rpx;
  352 + }
  353 +
  354 + .error-state {
  355 + text-align: center;
  356 + padding: 80rpx 40rpx;
  357 + color: #f56c6c;
  358 + font-size: 28rpx;
  359 + }
  360 +
  361 + .retry-btn {
  362 + margin-top: 32rpx;
  363 + }
  364 +
  365 + .info-sections {
  366 + display: flex;
  367 + flex-direction: column;
  368 + gap: 32rpx;
  369 + }
  370 +
  371 + .info-section {
  372 + background: #f9fff9;
  373 + border-radius: 24rpx;
  374 + padding: 32rpx;
  375 + border: 2rpx solid #e8f5e9;
  376 + }
  377 +
  378 + .section-title {
  379 + font-size: 32rpx;
  380 + font-weight: 600;
  381 + color: #2e7d32;
  382 + margin-bottom: 24rpx;
  383 + padding-bottom: 16rpx;
  384 + border-bottom: 2rpx solid #c8e6c9;
  385 + }
  386 +
  387 + .info-grid {
  388 + display: grid;
  389 + grid-template-columns: 1fr 1fr;
  390 + gap: 24rpx;
  391 + }
  392 +
  393 + .info-item {
  394 + display: flex;
  395 + flex-direction: column;
  396 + gap: 8rpx;
  397 + }
  398 +
  399 + .info-label {
  400 + font-size: 24rpx;
  401 + color: #6a9c6a;
  402 + font-weight: 500;
  403 + }
  404 +
  405 + .info-value {
  406 + font-size: 28rpx;
  407 + color: #2e7d32;
  408 + font-weight: 500;
  409 + word-break: break-all;
  410 + }
  411 +
  412 + .info-value.amount {
  413 + color: #f57c00;
  414 + font-weight: 600;
  415 + font-size: 32rpx;
  416 + }
  417 +
  418 + .status-badge {
  419 + display: inline-block;
  420 + padding: 8rpx 24rpx;
  421 + border-radius: 40rpx;
  422 + font-size: 24rpx;
  423 + font-weight: 500;
  424 + text-align: center;
  425 + width: fit-content;
  426 + }
  427 +
  428 + .status-badge.unapproved {
  429 + background: #fff3e0;
  430 + color: #ef6c00;
  431 + border: 2rpx solid #ffe0b2;
  432 + }
  433 +
  434 + .status-badge.pending {
  435 + background: #e3f2fd;
  436 + color: #1976d2;
  437 + border: 2rpx solid #bbdefb;
  438 + }
  439 +
  440 + .status-badge.approved {
  441 + background: #e8f5e9;
  442 + color: #2e7d32;
  443 + border: 2rpx solid #c8e6c9;
  444 + }
  445 +
  446 + .status-badge.rejected {
  447 + background: #ffebee;
  448 + color: #c62828;
  449 + border: 2rpx solid #ffcdd2;
  450 + }
  451 +
  452 + .purchase-records-list {
  453 + display: flex;
  454 + flex-direction: column;
  455 + gap: 16rpx;
  456 + }
  457 +
  458 + .purchase-record-item {
  459 + background: #fff;
  460 + border-radius: 16rpx;
  461 + padding: 24rpx;
  462 + border: 2rpx solid #e0e0e0;
  463 + }
  464 +
  465 + .record-header {
  466 + display: flex;
  467 + justify-content: space-between;
  468 + align-items: center;
  469 + margin-bottom: 16rpx;
  470 + padding-bottom: 16rpx;
  471 + border-bottom: 2rpx solid #f0f0f0;
  472 + }
  473 +
  474 + .record-category {
  475 + font-size: 28rpx;
  476 + color: #2e7d32;
  477 + font-weight: 600;
  478 + }
  479 +
  480 + .record-amount {
  481 + font-size: 32rpx;
  482 + color: #f57c00;
  483 + font-weight: 600;
  484 + }
  485 +
  486 + .record-details {
  487 + display: flex;
  488 + flex-wrap: wrap;
  489 + gap: 24rpx;
  490 + margin-bottom: 12rpx;
  491 + }
  492 +
  493 + .record-detail-item {
  494 + display: flex;
  495 + align-items: center;
  496 + gap: 8rpx;
  497 + }
  498 +
  499 + .record-label {
  500 + font-size: 24rpx;
  501 + color: #6a9c6a;
  502 + }
  503 +
  504 + .record-value {
  505 + font-size: 26rpx;
  506 + color: #2e7d32;
  507 + font-weight: 500;
  508 + }
  509 +
  510 + .record-memo {
  511 + margin-top: 12rpx;
  512 + padding-top: 12rpx;
  513 + border-top: 1rpx solid #f0f0f0;
  514 + display: flex;
  515 + gap: 8rpx;
  516 + }
  517 +
  518 + .record-memo .record-value {
  519 + flex: 1;
  520 + word-break: break-all;
  521 + }
  522 +
  523 + .record-attachment {
  524 + margin-top: 12rpx;
  525 + padding-top: 12rpx;
  526 + border-top: 1rpx solid #f0f0f0;
  527 + display: flex;
  528 + flex-direction: column;
  529 + gap: 8rpx;
  530 + }
  531 +
  532 + .attachment-list {
  533 + display: flex;
  534 + flex-direction: column;
  535 + gap: 8rpx;
  536 + margin-top: 8rpx;
  537 + }
  538 +
  539 + .attachment-item {
  540 + display: flex;
  541 + align-items: center;
  542 + gap: 8rpx;
  543 + padding: 8rpx 12rpx;
  544 + background: #f5f5f5;
  545 + border-radius: 8rpx;
  546 + cursor: pointer;
  547 + }
  548 +
  549 + .file-icon {
  550 + font-size: 24rpx;
  551 + }
  552 +
  553 + .file-name {
  554 + font-size: 24rpx;
  555 + color: #409EFF;
  556 + flex: 1;
  557 + overflow: hidden;
  558 + text-overflow: ellipsis;
  559 + white-space: nowrap;
  560 + }
  561 +</style>
  562 +
... ...
绿纤uni-app/pages/reimbursement-detail/reimbursement-detail.vue
... ... @@ -20,47 +20,41 @@
20 20 <view class="section-title">基本信息</view>
21 21 <view class="info-grid">
22 22 <view class="info-item">
23   - <text class="info-label">申请人</text>
24   - <text class="info-value">{{ detailData.applicationUserName || '无' }}</text>
  23 + <text class="info-label">申请人姓名</text>
  24 + <text class="info-value">{{ (formData && formData.applicationUserName) || detailData.applicationUserName || '无' }}</text>
25 25 </view>
26 26 <view class="info-item">
27 27 <text class="info-label">申请门店</text>
28   - <text class="info-value">{{ getStoreName(detailData.applicationStoreId) || '无' }}</text>
  28 + <text class="info-value">{{ formData ? (formData.applicationStoreName || getStoreName(formData.applicationStoreId)) : (getStoreName(detailData.applicationStoreId) || '无') }}</text>
29 29 </view>
30 30 <view class="info-item">
31 31 <text class="info-label">申请时间</text>
32   - <text class="info-value">{{ formatTime(detailData.applicationTime) }}</text>
  32 + <text class="info-value">{{ formatTime((formData && formData.applicationTime) || detailData.applicationTime) }}</text>
33 33 </view>
34 34 <view class="info-item">
35 35 <text class="info-label">总金额</text>
36   - <text class="info-value amount">¥{{ detailData.amount || 0 }}</text>
37   - </view>
38   - <view class="info-item">
39   - <text class="info-label">审批状态</text>
40   - <view class="status-badge" :class="detailData.approveStatus=='已审批'?'approved':detailData.approveStatus=='待审批'?'pending':detailData.approveStatus=='未通过'?'rejected':'unapproved'">
41   - {{ detailData.approveStatus || '未审批' }}
42   - </view>
  36 + <text class="info-value amount">¥{{ formatMoney((formData && formData.amount) || detailData.amount) }}</text>
43 37 </view>
44 38 </view>
45 39 </view>
46 40  
47   - <!-- 购买物品清单 -->
  41 + <!-- 购买记录列表 -->
48 42 <view class="info-section" v-if="purchaseRecords && purchaseRecords.length > 0">
49   - <view class="section-title">购买物品清单</view>
  43 + <view class="section-title">购买记录列表</view>
50 44 <view class="purchase-records-list">
51 45 <view v-for="(item, index) in purchaseRecords" :key="item.id || index" class="purchase-record-item">
52 46 <view class="record-header">
53 47 <text class="record-category">{{ item.reimbursementCategoryName || '无' }}</text>
54   - <text class="record-amount">¥{{ item.amount || 0 }}</text>
  48 + <text class="record-amount">¥{{ formatMoney(item.amount) }}</text>
55 49 </view>
56 50 <view class="record-details">
57 51 <view class="record-detail-item">
58 52 <text class="record-label">单价:</text>
59   - <text class="record-value">¥{{ item.unitPrice || 0 }}</text>
  53 + <text class="record-value">¥{{ formatMoney(item.unitPrice) }}</text>
60 54 </view>
61 55 <view class="record-detail-item">
62 56 <text class="record-label">数量:</text>
63   - <text class="record-value">{{ item.quantity || 0 }}</text>
  57 + <text class="record-value">{{ item.quantity || '无' }}</text>
64 58 </view>
65 59 <view class="record-detail-item">
66 60 <text class="record-label">购买时间:</text>
... ... @@ -68,20 +62,38 @@
68 62 </view>
69 63 </view>
70 64 <view class="record-memo" v-if="item.memo">
71   - <text class="record-label">备注:</text>
72   - <text class="record-value">{{ item.memo }}</text>
  65 + <text class="record-label">备注说明:</text>
  66 + <text class="record-value">{{ item.memo || '无' }}</text>
73 67 </view>
74 68 <!-- 附件信息 -->
75 69 <view class="record-attachment" v-if="item.attachment && item.attachment.length > 0">
76 70 <text class="record-label">附件:</text>
77 71 <view class="attachment-list">
78   - <view
79   - v-for="(file, fileIndex) in item.attachment"
80   - :key="fileIndex"
81   - class="attachment-item"
82   - @click="previewFile(file, item.attachment)">
  72 + <view class="attachment-count">
83 73 <text class="file-icon">📎</text>
84   - <text class="file-name">{{ file.name || '文件' }}</text>
  74 + <text class="file-count">{{ item.attachment.length }}个文件</text>
  75 + </view>
  76 + <!-- 显示所有附件 -->
  77 + <view class="attachment-items">
  78 + <view
  79 + v-for="(file, fileIndex) in item.attachment"
  80 + :key="fileIndex"
  81 + class="attachment-item"
  82 + @click="previewFile(file, item.attachment)">
  83 + <!-- 如果是图片,显示预览 -->
  84 + <view v-if="isImageFile(file)" class="attachment-image-wrapper">
  85 + <image
  86 + :src="getImageUrl(file.url)"
  87 + mode="aspectFit"
  88 + class="attachment-image"
  89 + />
  90 + </view>
  91 + <!-- 如果不是图片,显示文件信息 -->
  92 + <view v-else class="attachment-file-info">
  93 + <text class="file-icon">📄</text>
  94 + <text class="file-name">{{ file.name || '文件' + (fileIndex + 1) }}</text>
  95 + </view>
  96 + </view>
85 97 </view>
86 98 </view>
87 99 </view>
... ... @@ -89,17 +101,137 @@
89 101 </view>
90 102 </view>
91 103  
92   - <!-- 审批信息 -->
93   - <view class="info-section" v-if="detailData.approveStatus && detailData.approveStatus !== '未审批'">
94   - <view class="section-title">审批信息</view>
95   - <view class="info-grid">
96   - <view class="info-item" v-if="detailData.approveUser">
97   - <text class="info-label">审批人</text>
98   - <text class="info-value">{{ detailData.approveUserName || detailData.approveUser || '无' }}</text>
  104 + <!-- 审批流程 -->
  105 + <view class="info-section" v-if="nodes && nodes.length > 0">
  106 + <view class="section-title">审批流程</view>
  107 + <view class="approval-flow">
  108 + <view
  109 + v-for="(node, index) in nodes"
  110 + :key="node.nodeId || index"
  111 + class="flow-node"
  112 + :class="{
  113 + 'node-active': node.nodeOrder === currentNodeOrder,
  114 + 'node-completed': node.nodeOrder < currentNodeOrder,
  115 + 'node-pending': node.nodeOrder > currentNodeOrder
  116 + }"
  117 + >
  118 + <view class="node-header">
  119 + <view class="node-order">{{ node.nodeOrder }}</view>
  120 + <view class="node-info">
  121 + <view class="node-name">{{ node.nodeName || '无' }}</view>
  122 + <view class="node-type">
  123 + <view class="node-type-tag" :class="node.approvalType === '会签' ? 'type-success' : 'type-warning'">
  124 + {{ node.approvalType || '无' }}
  125 + </view>
  126 + <view v-if="node.isRequired === 1" class="node-type-tag type-info">必审</view>
  127 + </view>
  128 + </view>
  129 + </view>
  130 +
  131 + <!-- 审批人列表 -->
  132 + <view class="approvers-list" v-if="node.approvers && node.approvers.length > 0">
  133 + <text class="approvers-label">审批人:</text>
  134 + <view class="approvers-items">
  135 + <view
  136 + v-for="approver in node.approvers"
  137 + :key="approver.userId"
  138 + class="approver-tag"
  139 + >
  140 + {{ approver.userName || '无' }}
  141 + </view>
  142 + </view>
  143 + </view>
  144 +
  145 + <!-- 审批记录 -->
  146 + <view class="approval-records" v-if="node.approvalRecords && node.approvalRecords.length > 0">
  147 + <text class="records-label">审批记录:</text>
  148 + <view class="records-list">
  149 + <view
  150 + v-for="(record, recordIndex) in node.approvalRecords"
  151 + :key="recordIndex"
  152 + class="record-item"
  153 + >
  154 + <view class="record-header">
  155 + <text class="record-approver">{{ record.approverName || '无' }}</text>
  156 + <view class="record-result-tag" :class="getRecordClass(record.approvalResult)">
  157 + {{ record.approvalResult || '无' }}
  158 + </view>
  159 + </view>
  160 + <view class="record-opinion" v-if="record.approvalOpinion">
  161 + 意见:{{ record.approvalOpinion }}
  162 + </view>
  163 + <view class="record-time" v-if="record.approvalTime">
  164 + 时间:{{ formatTime(record.approvalTime) }}
  165 + </view>
  166 + </view>
  167 + </view>
  168 + </view>
99 169 </view>
100   - <view class="info-item" v-if="detailData.approveTime">
101   - <text class="info-label">审批时间</text>
102   - <text class="info-value">{{ formatTime(detailData.approveTime) }}</text>
  170 + </view>
  171 + </view>
  172 +
  173 + <!-- 当前审批人信息 -->
  174 + <view class="info-section" v-if="currentApprovers && currentApprovers.length > 0">
  175 + <view class="section-title">当前审批人</view>
  176 + <view class="current-approvers">
  177 + <view
  178 + v-for="approver in currentApprovers"
  179 + :key="approver.userId"
  180 + class="current-approver-tag"
  181 + >
  182 + {{ approver.userName || '无' }}
  183 + </view>
  184 + </view>
  185 + </view>
  186 +
  187 + <!-- 退回原因 -->
  188 + <view class="info-section" v-if="returnedReason">
  189 + <view class="section-title">退回原因</view>
  190 + <view class="returned-reason">
  191 + {{ returnedReason }}
  192 + </view>
  193 + </view>
  194 +
  195 + <!-- 审批操作区域 -->
  196 + <view class="info-section" v-if="canApprove">
  197 + <view class="section-title">审批操作</view>
  198 + <view class="approval-form">
  199 + <view class="form-item">
  200 + <text class="form-label">审批结果</text>
  201 + <view class="radio-group">
  202 + <view
  203 + class="radio-item"
  204 + :class="{ 'active': approvalForm.result === '通过' }"
  205 + @tap="approvalForm.result = '通过'">
  206 + <text>通过</text>
  207 + </view>
  208 + <view
  209 + class="radio-item"
  210 + :class="{ 'active': approvalForm.result === '不通过' }"
  211 + @tap="approvalForm.result = '不通过'">
  212 + <text>不通过</text>
  213 + </view>
  214 + <view
  215 + class="radio-item"
  216 + :class="{ 'active': approvalForm.result === '退回' }"
  217 + @tap="approvalForm.result = '退回'">
  218 + <text>退回</text>
  219 + </view>
  220 + </view>
  221 + </view>
  222 + <view class="form-item">
  223 + <text class="form-label">审批意见</text>
  224 + <textarea
  225 + v-model="approvalForm.opinion"
  226 + placeholder="请输入审批意见"
  227 + class="form-textarea"
  228 + maxlength="500"
  229 + />
  230 + </view>
  231 + <view class="form-actions">
  232 + <button class="approve-btn" @tap="handleApprove" :disabled="approving">
  233 + {{ approving ? '提交中...' : '提交审批' }}
  234 + </button>
103 235 </view>
104 236 </view>
105 237 </view>
... ... @@ -120,16 +252,55 @@
120 252 loading: false,
121 253 error: null,
122 254 detailData: null,
  255 + formData: null, // 表单数据
  256 + nodes: [], // 审批节点
  257 + currentApprovers: [], // 当前审批人
  258 + currentNodeOrder: 0, // 当前节点顺序
  259 + approvalStatus: '', // 审批状态
  260 + returnedReason: null, // 退回原因
123 261 purchaseRecords: [],
124 262 reimbursementId: null,
125 263 storeOptions: [],
126   - baseUrl: config.getApiBaseUrl()
  264 + baseUrl: config.getApiBaseUrl(),
  265 + userInfo: null, // 当前登录用户信息
  266 + // 审批操作表单
  267 + approvalForm: {
  268 + result: '通过',
  269 + opinion: ''
  270 + },
  271 + approving: false // 审批提交中
  272 + }
  273 + },
  274 +
  275 + computed: {
  276 + // 判断是否可以审批
  277 + canApprove() {
  278 + // 判断是否可以审批:状态为"审批中"且当前用户是当前审批人
  279 + if (this.approvalStatus !== '审批中') {
  280 + return false
  281 + }
  282 +
  283 + if (!this.userInfo || !this.userInfo.userId) {
  284 + return false
  285 + }
  286 +
  287 + if (!this.currentApprovers || this.currentApprovers.length === 0) {
  288 + return false
  289 + }
  290 +
  291 + // 检查当前用户的userId是否在审批人列表中
  292 + const isCurrentApprover = this.currentApprovers.some(approver => {
  293 + return approver.userId === this.userInfo.userId
  294 + })
  295 +
  296 + return isCurrentApprover
127 297 }
128 298 },
129 299  
130 300 onLoad(options) {
131 301 if (options.id) {
132 302 this.reimbursementId = options.id
  303 + this.userInfo = uni.getStorageSync('userInfo')
133 304 this.loadStoreOptions()
134 305 this.loadDetail()
135 306 } else {
... ... @@ -174,10 +345,34 @@
174 345 const res = await api.getReimbursementDetail(this.reimbursementId)
175 346  
176 347 if (res.code === 200 && res.data) {
177   - this.detailData = res.data
  348 + // 根据PC端的数据结构处理
  349 + this.formData = res.data.form || res.data
  350 + this.detailData = this.formData // 保持兼容
  351 + this.nodes = res.data.nodes || []
  352 + this.currentApprovers = res.data.currentApprovers || []
  353 + this.currentNodeOrder = res.data.currentNodeOrder || 0
  354 + this.approvalStatus = res.data.approvalStatus || this.formData.approveStatus || this.formData.approvalStatus || ''
  355 + this.returnedReason = res.data.returnedReason || null
178 356  
179   - // 加载关联的购买记录
180   - if (this.detailData.purchaseRecordsId) {
  357 + // 处理购买记录
  358 + if (res.data.purchaseRecords && res.data.purchaseRecords.length > 0) {
  359 + this.purchaseRecords = res.data.purchaseRecords.map(item => {
  360 + // 处理附件字段
  361 + if (item.attachment) {
  362 + try {
  363 + item.attachment = typeof item.attachment === 'string'
  364 + ? JSON.parse(item.attachment)
  365 + : item.attachment
  366 + } catch (e) {
  367 + item.attachment = []
  368 + }
  369 + } else {
  370 + item.attachment = []
  371 + }
  372 + return item
  373 + })
  374 + } else if (this.detailData.purchaseRecordsId) {
  375 + // 如果没有直接返回购买记录,则通过ID加载
181 376 await this.loadPurchaseRecords(this.detailData.purchaseRecordsId)
182 377 }
183 378 } else {
... ... @@ -231,30 +426,129 @@
231 426 // 格式化时间
232 427 formatTime(timestamp) {
233 428 if (!timestamp) return '无'
  429 + // 如果是时间戳(数字),需要转换
  430 + if (typeof timestamp === 'number') {
  431 + const date = new Date(timestamp)
  432 + const year = date.getFullYear()
  433 + const month = String(date.getMonth() + 1).padStart(2, '0')
  434 + const day = String(date.getDate()).padStart(2, '0')
  435 + const hours = String(date.getHours()).padStart(2, '0')
  436 + const minutes = String(date.getMinutes()).padStart(2, '0')
  437 + const seconds = String(date.getSeconds()).padStart(2, '0')
  438 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  439 + }
  440 + // 如果是字符串,尝试解析
234 441 const date = new Date(timestamp)
235   - return date.toLocaleString('zh-CN', {
236   - year: 'numeric',
237   - month: '2-digit',
238   - day: '2-digit',
239   - hour: '2-digit',
240   - minute: '2-digit'
  442 + if (isNaN(date.getTime())) return timestamp
  443 + const year = date.getFullYear()
  444 + const month = String(date.getMonth() + 1).padStart(2, '0')
  445 + const day = String(date.getDate()).padStart(2, '0')
  446 + const hours = String(date.getHours()).padStart(2, '0')
  447 + const minutes = String(date.getMinutes()).padStart(2, '0')
  448 + const seconds = String(date.getSeconds()).padStart(2, '0')
  449 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  450 + },
  451 +
  452 + // 格式化金额
  453 + formatMoney(amount) {
  454 + if (!amount) return '0.00'
  455 + return Number(amount).toLocaleString('zh-CN', {
  456 + minimumFractionDigits: 2,
  457 + maximumFractionDigits: 2
241 458 })
242 459 },
  460 +
  461 + // 获取图片URL
  462 + getImageUrl(url) {
  463 + if (!url) return ''
  464 + if (url.startsWith('http')) return url
  465 + return `${this.baseUrl}${url}`
  466 + },
  467 +
  468 + // 判断是否为图片文件
  469 + isImageFile(file) {
  470 + if (!file || !file.url) return false
  471 + const url = file.url.toLowerCase()
  472 + const name = (file.name || '').toLowerCase()
  473 + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
  474 + return imageExtensions.some(ext => url.includes(ext) || name.includes(ext))
  475 + },
243 476  
244 477 // 获取状态样式类
245 478 getStatusClass(status) {
246 479 switch (status) {
247   - case '已审批':
248   - return 'approved'
249 480 case '待审批':
250 481 return 'pending'
  482 + case '审批中':
  483 + return 'pending'
  484 + case '已通过':
  485 + return 'approved'
251 486 case '未通过':
252 487 return 'rejected'
253   - case '未审批':
  488 + case '已退回':
  489 + return 'rejected'
254 490 default:
255 491 return 'unapproved'
256 492 }
257 493 },
  494 +
  495 + // 获取审批记录样式类
  496 + getRecordClass(result) {
  497 + switch (result) {
  498 + case '通过':
  499 + return 'result-success'
  500 + case '不通过':
  501 + return 'result-danger'
  502 + case '退回':
  503 + return 'result-warning'
  504 + case '待审批':
  505 + default:
  506 + return 'result-info'
  507 + }
  508 + },
  509 +
  510 + // 提交审批
  511 + async handleApprove() {
  512 + if (!this.approvalForm.result) {
  513 + uni.showToast({
  514 + title: '请选择审批结果',
  515 + icon: 'none'
  516 + })
  517 + return
  518 + }
  519 +
  520 + this.approving = true
  521 + try {
  522 + const res = await api.approveReimbursement(this.reimbursementId, {
  523 + result: this.approvalForm.result,
  524 + opinion: this.approvalForm.opinion || ''
  525 + })
  526 +
  527 + if (res.code === 200) {
  528 + uni.showToast({
  529 + title: '审批成功',
  530 + icon: 'success'
  531 + })
  532 + // 重新加载详情
  533 + setTimeout(() => {
  534 + this.loadDetail()
  535 + }, 1500)
  536 + } else {
  537 + uni.showToast({
  538 + title: res.message || '审批失败',
  539 + icon: 'none'
  540 + })
  541 + }
  542 + } catch (error) {
  543 + console.error('审批失败:', error)
  544 + uni.showToast({
  545 + title: '审批失败',
  546 + icon: 'none'
  547 + })
  548 + } finally {
  549 + this.approving = false
  550 + }
  551 + },
258 552 // 预览文件
259 553 previewFile(file, attachmentList) {
260 554 if (!file || !file.url) {
... ... @@ -536,27 +830,393 @@
536 830 margin-top: 8rpx;
537 831 }
538 832  
539   - .attachment-item {
  833 + .attachment-count {
540 834 display: flex;
541 835 align-items: center;
542 836 gap: 8rpx;
543   - padding: 8rpx 12rpx;
544   - background: #f5f5f5;
545   - border-radius: 8rpx;
546   - cursor: pointer;
  837 + margin-bottom: 16rpx;
547 838 }
548 839  
549 840 .file-icon {
550 841 font-size: 24rpx;
551 842 }
552 843  
553   - .file-name {
  844 + .file-count {
554 845 font-size: 24rpx;
555   - color: #409EFF;
  846 + color: #606266;
  847 + }
  848 +
  849 + .attachment-items {
  850 + display: flex;
  851 + flex-wrap: wrap;
  852 + gap: 16rpx;
  853 + margin-top: 16rpx;
  854 + }
  855 +
  856 + .attachment-item {
  857 + display: flex;
  858 + align-items: center;
  859 + justify-content: center;
  860 + cursor: pointer;
  861 + transition: all 0.2s;
  862 + }
  863 +
  864 + .attachment-item:active {
  865 + opacity: 0.7;
  866 + transform: scale(0.98);
  867 + }
  868 +
  869 + .attachment-image-wrapper {
  870 + width: 200rpx;
  871 + height: 200rpx;
  872 + border-radius: 12rpx;
  873 + overflow: hidden;
  874 + border: 2rpx solid #e0e0e0;
  875 + background: #f5f5f5;
  876 + }
  877 +
  878 + .attachment-image {
  879 + width: 100%;
  880 + height: 100%;
  881 + }
  882 +
  883 + .attachment-file-info {
  884 + display: flex;
  885 + align-items: center;
  886 + gap: 12rpx;
  887 + padding: 16rpx 24rpx;
  888 + background: #f9fff9;
  889 + border: 2rpx solid #c8e6c9;
  890 + border-radius: 12rpx;
  891 + min-width: 200rpx;
  892 + }
  893 +
  894 + .attachment-file-info .file-icon {
  895 + font-size: 32rpx;
  896 + }
  897 +
  898 + .attachment-file-info .file-name {
  899 + font-size: 24rpx;
  900 + color: #2e7d32;
556 901 flex: 1;
557 902 overflow: hidden;
558 903 text-overflow: ellipsis;
559 904 white-space: nowrap;
560 905 }
  906 +
  907 + /* 审批流程样式 */
  908 + .approval-flow {
  909 + display: flex;
  910 + flex-direction: column;
  911 + gap: 24rpx;
  912 + }
  913 +
  914 + .flow-node {
  915 + padding: 24rpx;
  916 + border: 2rpx solid #e4e7ed;
  917 + border-radius: 16rpx;
  918 + background: #fafafa;
  919 + transition: all 0.3s;
  920 + }
  921 +
  922 + .flow-node.node-active {
  923 + border-color: #409EFF;
  924 + background: #ecf5ff;
  925 + }
  926 +
  927 + .flow-node.node-completed {
  928 + border-color: #67C23A;
  929 + background: #f0f9ff;
  930 + }
  931 +
  932 + .flow-node.node-pending {
  933 + border-color: #e4e7ed;
  934 + background: #fafafa;
  935 + opacity: 0.6;
  936 + }
  937 +
  938 + .node-header {
  939 + display: flex;
  940 + align-items: center;
  941 + margin-bottom: 24rpx;
  942 + }
  943 +
  944 + .node-order {
  945 + width: 64rpx;
  946 + height: 64rpx;
  947 + line-height: 64rpx;
  948 + text-align: center;
  949 + background: #409EFF;
  950 + color: #fff;
  951 + border-radius: 50%;
  952 + font-weight: 600;
  953 + font-size: 28rpx;
  954 + margin-right: 24rpx;
  955 + flex-shrink: 0;
  956 + }
  957 +
  958 + .node-info {
  959 + flex: 1;
  960 + }
  961 +
  962 + .node-name {
  963 + font-size: 32rpx;
  964 + font-weight: 600;
  965 + color: #303133;
  966 + margin-bottom: 16rpx;
  967 + }
  968 +
  969 + .node-type {
  970 + display: flex;
  971 + gap: 16rpx;
  972 + }
  973 +
  974 + .node-type-tag {
  975 + display: inline-block;
  976 + padding: 4rpx 16rpx;
  977 + border-radius: 8rpx;
  978 + font-size: 22rpx;
  979 + font-weight: 500;
  980 + }
  981 +
  982 + .node-type-tag.type-success {
  983 + background: #f0f9ff;
  984 + color: #67C23A;
  985 + border: 2rpx solid #c8e6c9;
  986 + }
  987 +
  988 + .node-type-tag.type-warning {
  989 + background: #fff3e0;
  990 + color: #e6a23c;
  991 + border: 2rpx solid #ffe0b2;
  992 + }
  993 +
  994 + .node-type-tag.type-info {
  995 + background: #e3f2fd;
  996 + color: #909399;
  997 + border: 2rpx solid #bbdefb;
  998 + }
  999 +
  1000 + .approvers-list {
  1001 + margin-bottom: 24rpx;
  1002 + display: flex;
  1003 + align-items: flex-start;
  1004 + gap: 16rpx;
  1005 + }
  1006 +
  1007 + .approvers-label {
  1008 + min-width: 160rpx;
  1009 + color: #606266;
  1010 + font-weight: 500;
  1011 + font-size: 26rpx;
  1012 + }
  1013 +
  1014 + .approvers-items {
  1015 + flex: 1;
  1016 + display: flex;
  1017 + flex-wrap: wrap;
  1018 + gap: 12rpx;
  1019 + }
  1020 +
  1021 + .approver-tag {
  1022 + display: inline-block;
  1023 + padding: 8rpx 16rpx;
  1024 + background: #e8f5e9;
  1025 + border: 2rpx solid #c8e6c9;
  1026 + border-radius: 16rpx;
  1027 + font-size: 24rpx;
  1028 + color: #2e7d32;
  1029 + }
  1030 +
  1031 + .approval-records {
  1032 + margin-top: 24rpx;
  1033 + padding-top: 24rpx;
  1034 + border-top: 2rpx solid #e4e7ed;
  1035 + }
  1036 +
  1037 + .records-label {
  1038 + color: #606266;
  1039 + font-weight: 500;
  1040 + font-size: 26rpx;
  1041 + margin-bottom: 16rpx;
  1042 + display: block;
  1043 + }
  1044 +
  1045 + .records-list {
  1046 + display: flex;
  1047 + flex-direction: column;
  1048 + gap: 16rpx;
  1049 + }
  1050 +
  1051 + .record-item {
  1052 + padding: 16rpx;
  1053 + background: #fff;
  1054 + border-radius: 8rpx;
  1055 + border-left: 6rpx solid #409EFF;
  1056 + }
  1057 +
  1058 + .record-header {
  1059 + display: flex;
  1060 + justify-content: space-between;
  1061 + align-items: center;
  1062 + margin-bottom: 8rpx;
  1063 + }
  1064 +
  1065 + .record-approver {
  1066 + font-weight: 500;
  1067 + color: #303133;
  1068 + font-size: 26rpx;
  1069 + }
  1070 +
  1071 + .record-result-tag {
  1072 + display: inline-block;
  1073 + padding: 4rpx 12rpx;
  1074 + border-radius: 8rpx;
  1075 + font-size: 22rpx;
  1076 + font-weight: 500;
  1077 + }
  1078 +
  1079 + .record-result-tag.result-success {
  1080 + background: #f0f9ff;
  1081 + color: #67C23A;
  1082 + }
  1083 +
  1084 + .record-result-tag.result-danger {
  1085 + background: #ffebee;
  1086 + color: #F56C6C;
  1087 + }
  1088 +
  1089 + .record-result-tag.result-warning {
  1090 + background: #fff3e0;
  1091 + color: #e6a23c;
  1092 + }
  1093 +
  1094 + .record-result-tag.result-info {
  1095 + background: #e3f2fd;
  1096 + color: #909399;
  1097 + }
  1098 +
  1099 + .record-opinion {
  1100 + color: #606266;
  1101 + font-size: 24rpx;
  1102 + margin-bottom: 8rpx;
  1103 + line-height: 1.6;
  1104 + }
  1105 +
  1106 + .record-time {
  1107 + color: #909399;
  1108 + font-size: 22rpx;
  1109 + }
  1110 +
  1111 + /* 当前审批人样式 */
  1112 + .current-approvers {
  1113 + padding: 24rpx;
  1114 + background: #fff3cd;
  1115 + border-radius: 16rpx;
  1116 + display: flex;
  1117 + flex-wrap: wrap;
  1118 + gap: 16rpx;
  1119 + }
  1120 +
  1121 + .current-approver-tag {
  1122 + display: inline-block;
  1123 + padding: 12rpx 24rpx;
  1124 + background: #fff;
  1125 + border: 2rpx solid #e6a23c;
  1126 + border-radius: 16rpx;
  1127 + font-size: 26rpx;
  1128 + color: #e6a23c;
  1129 + font-weight: 500;
  1130 + }
  1131 +
  1132 + /* 退回原因样式 */
  1133 + .returned-reason {
  1134 + padding: 24rpx;
  1135 + background: #fef0f0;
  1136 + border-radius: 16rpx;
  1137 + color: #F56C6C;
  1138 + line-height: 1.6;
  1139 + font-size: 26rpx;
  1140 + }
  1141 +
  1142 + /* 审批操作表单样式 */
  1143 + .approval-form {
  1144 + display: flex;
  1145 + flex-direction: column;
  1146 + gap: 32rpx;
  1147 + }
  1148 +
  1149 + .form-item {
  1150 + display: flex;
  1151 + flex-direction: column;
  1152 + gap: 16rpx;
  1153 + }
  1154 +
  1155 + .form-label {
  1156 + font-size: 28rpx;
  1157 + color: #2e7d32;
  1158 + font-weight: 600;
  1159 + }
  1160 +
  1161 + .radio-group {
  1162 + display: flex;
  1163 + gap: 24rpx;
  1164 + }
  1165 +
  1166 + .radio-item {
  1167 + flex: 1;
  1168 + padding: 20rpx;
  1169 + background: #f9fff9;
  1170 + border: 3rpx solid #c8e6c9;
  1171 + border-radius: 16rpx;
  1172 + text-align: center;
  1173 + font-size: 26rpx;
  1174 + color: #2e7d32;
  1175 + transition: all 0.2s;
  1176 + }
  1177 +
  1178 + .radio-item.active {
  1179 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  1180 + border-color: #43e97b;
  1181 + color: #fff;
  1182 + font-weight: 600;
  1183 + }
  1184 +
  1185 + .form-textarea {
  1186 + width: 100%;
  1187 + min-height: 200rpx;
  1188 + padding: 24rpx;
  1189 + background: #f9fff9;
  1190 + border: 3rpx solid #c8e6c9;
  1191 + border-radius: 16rpx;
  1192 + font-size: 26rpx;
  1193 + color: #2e7d32;
  1194 + box-sizing: border-box;
  1195 + }
  1196 +
  1197 + .form-actions {
  1198 + margin-top: 16rpx;
  1199 + }
  1200 +
  1201 + .approve-btn {
  1202 + width: 100%;
  1203 + padding:10rpx 32rpx;
  1204 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  1205 + color: #fff;
  1206 + border-radius: 48rpx;
  1207 + font-size: 32rpx;
  1208 + font-weight: 600;
  1209 + border: none;
  1210 + box-shadow: 0 8rpx 24rpx rgba(67, 233, 123, 0.3);
  1211 + }
  1212 +
  1213 + .approve-btn:active {
  1214 + transform: scale(0.98);
  1215 + box-shadow: 0 4rpx 12rpx rgba(67, 233, 123, 0.4);
  1216 + }
  1217 +
  1218 + .approve-btn:disabled {
  1219 + opacity: 0.5;
  1220 + }
561 1221 </style>
562 1222  
... ...
绿纤uni-app/pages/reimbursement-form/reimbursement-form - 副本.vue 0 → 100644
  1 +<template>
  2 + <view class="form-container">
  3 + <view class="form-card">
  4 + <view class="form-content">
  5 + <form @submit="handleFormSubmit">
  6 + <!-- 申请人 -->
  7 + <!-- <view class="form-group">
  8 + <text class="form-label">申请人</text>
  9 + <view class="input-wrapper">
  10 + <u-input v-model="formData.applicationUserName" placeholder="自动填充" readonly
  11 + class="select-input" />
  12 + </view>
  13 + </view> -->
  14 +
  15 + <!-- 申请门店 -->
  16 + <!-- <view class="form-group">
  17 + <text class="form-label">申请门店</text>
  18 + <view class="input-wrapper">
  19 + <u-input v-model="formData.applicationStoreName" placeholder="自动填充" readonly
  20 + class="select-input" />
  21 + </view>
  22 + </view> -->
  23 +
  24 + <!-- 申请时间 -->
  25 + <view class="form-group">
  26 + <text class="form-label">申请时间</text>
  27 + <view class="input-wrapper">
  28 + <picker mode="date" :value="formData.applicationTimeStr" @change="onApplicationTimeChange">
  29 + <view class="custom-select">
  30 + <text class="select-text">{{ formData.applicationTimeStr || '请选择申请时间' }}</text>
  31 + <text class="select-arrow">▼</text>
  32 + </view>
  33 + </picker>
  34 + </view>
  35 + </view>
  36 +
  37 + <!-- 总金额 -->
  38 + <view class="form-group">
  39 + <text class="form-label">总金额</text>
  40 + <view class="input-wrapper">
  41 + <u-input v-model="formData.amount" placeholder="自动计算" type="digit" readonly
  42 + class="select-input" />
  43 + </view>
  44 + </view>
  45 +
  46 + <!-- 购买物品清单 -->
  47 + <view class="form-group">
  48 + <text class="form-label">购买物品清单</text>
  49 + <view class="input-wrapper">
  50 + <view class="purchase-select-btn" @tap="openPurchaseDialog">
  51 + <text class="btn-text">选择购买物品</text>
  52 + </view>
  53 + <text v-if="selectedPurchaseRecords.length > 0" class="selected-count">
  54 + 已选择 {{ selectedPurchaseRecords.length }} 条记录
  55 + </text>
  56 + </view>
  57 + </view>
  58 +
  59 + <!-- 已选择的购买物品清单 -->
  60 + <view class="form-group" v-if="selectedPurchaseRecords.length > 0">
  61 + <view class="purchase-list">
  62 + <view v-for="(item, index) in selectedPurchaseRecords" :key="item.id || index" class="purchase-item">
  63 + <view class="purchase-item-header">
  64 + <text class="purchase-category">{{ item.reimbursementCategoryName || '无' }}</text>
  65 + <view class="purchase-remove" @tap="removePurchaseRecord(index)">
  66 + <text class="remove-icon">✕</text>
  67 + </view>
  68 + </view>
  69 + <view class="purchase-item-details">
  70 + <text class="purchase-detail">单价: ¥{{ item.unitPrice || 0 }}</text>
  71 + <text class="purchase-detail">数量: {{ item.quantity || 0 }}</text>
  72 + <text class="purchase-detail amount">金额: ¥{{ item.amount || 0 }}</text>
  73 + </view>
  74 + <view class="purchase-item-time">
  75 + <text class="purchase-time">购买时间: {{ formatTime(item.purchaseTime) }}</text>
  76 + </view>
  77 + </view>
  78 + </view>
  79 + </view>
  80 +
  81 + <!-- 提交按钮 -->
  82 + <view class="btn-group">
  83 + <button type="submit" class="btn btn-primary"
  84 + :style="{opacity: isSubmitting ? 0.5 : 1}"
  85 + @tap="isSubmitting ? null : handleFormSubmit()">
  86 + {{ isSubmitting ? '提交中...' : (formData.id ? '更新' : '提交') }}
  87 + </button>
  88 + </view>
  89 + </form>
  90 + </view>
  91 + </view>
  92 +
  93 + <!-- 选择购买物品弹窗 -->
  94 + <view v-if="showPurchaseDialog" class="modal-overlay" @tap="closePurchaseDialog">
  95 + <view class="modal-content" @tap.stop>
  96 + <view class="modal-header">
  97 + <text class="modal-title">选择购买物品</text>
  98 + <view class="modal-close" @tap="closePurchaseDialog">
  99 + <text class="close-icon">✕</text>
  100 + </view>
  101 + </view>
  102 + <view class="modal-body">
  103 + <!-- 搜索框 -->
  104 + <view class="search-box">
  105 + <u-input v-model="searchKeyword" placeholder="搜索分类名称" @input="handleSearch"
  106 + class="search-input" />
  107 + </view>
  108 + <!-- 购买记录列表 -->
  109 + <scroll-view scroll-y class="purchase-dialog-list" @scrolltolower="loadMorePurchaseRecords">
  110 + <view v-if="purchaseListLoading" class="loading-text">加载中...</view>
  111 + <view v-else-if="filteredPurchaseList.length === 0" class="empty-text">暂无可用记录</view>
  112 + <view v-else>
  113 + <view
  114 + v-for="(item, index) in filteredPurchaseList"
  115 + :key="item.id || index"
  116 + class="purchase-dialog-item"
  117 + :class="{ 'selected': isPurchaseSelected(item.id) }"
  118 + @tap="togglePurchaseSelection(item)">
  119 + <view class="purchase-dialog-header">
  120 + <text class="purchase-dialog-category">{{ item.reimbursementCategoryName || '无' }}</text>
  121 + <view v-if="isPurchaseSelected(item.id)" class="selected-mark">✓</view>
  122 + </view>
  123 + <view class="purchase-dialog-details">
  124 + <text class="purchase-dialog-detail">单价: ¥{{ item.unitPrice || 0 }}</text>
  125 + <text class="purchase-dialog-detail">数量: {{ item.quantity || 0 }}</text>
  126 + <text class="purchase-dialog-detail amount">金额: ¥{{ item.amount || 0 }}</text>
  127 + </view>
  128 + <view class="purchase-dialog-time">
  129 + <text>{{ formatTime(item.purchaseTime) }}</text>
  130 + </view>
  131 + </view>
  132 + </view>
  133 + </scroll-view>
  134 + </view>
  135 + <view class="modal-footer">
  136 + <button class="modal-btn cancel-btn" @tap="closePurchaseDialog">取消</button>
  137 + <button class="modal-btn confirm-btn" @tap="confirmPurchaseSelection">确定</button>
  138 + </view>
  139 + </view>
  140 + </view>
  141 + </view>
  142 +</template>
  143 +
  144 +<script>
  145 + import api from '@/apis/index.js'
  146 + import purchaseApi from '@/apis/modules/purchase.js'
  147 +
  148 + export default {
  149 + data() {
  150 + return {
  151 + isSubmitting: false,
  152 + formData: {
  153 + id: undefined,
  154 + applicationUserId: undefined,
  155 + applicationUserName: undefined,
  156 + applicationStoreId: undefined,
  157 + applicationStoreName: undefined,
  158 + applicationTime: undefined,
  159 + applicationTimeStr: undefined,
  160 + amount: undefined,
  161 + approveStatus: '未审批',
  162 + selectedPurchaseRecordIds: []
  163 + },
  164 + selectedPurchaseRecords: [],
  165 + // 购买物品选择弹窗相关
  166 + showPurchaseDialog: false,
  167 + purchaseList: [],
  168 + filteredPurchaseList: [],
  169 + purchaseListLoading: false,
  170 + tempSelectedPurchaseIds: [],
  171 + searchKeyword: '',
  172 + userInfo: null,
  173 + newuserInfo: null
  174 + }
  175 + },
  176 +
  177 + onLoad(options) {
  178 + this.initializePage(options)
  179 + },
  180 +
  181 + methods: {
  182 + // 初始化页面
  183 + async initializePage(options) {
  184 + try {
  185 + // 获取用户信息
  186 + this.userInfo = uni.getStorageSync('userInfo')
  187 + this.newuserInfo = uni.getStorageSync('newuserInfo')
  188 +
  189 + if (!this.userInfo || Object.keys(this.userInfo).length === 0) {
  190 + uni.showToast({
  191 + title: '请先登录',
  192 + icon: 'none'
  193 + })
  194 + setTimeout(() => {
  195 + uni.reLaunch({
  196 + url: '/pages/login/login'
  197 + })
  198 + }, 1500)
  199 + return
  200 + }
  201 +
  202 + // 设置默认值
  203 + this.setDefaultValues()
  204 +
  205 + // 如果有ID,加载编辑数据
  206 + if (options.id) {
  207 + await this.loadEditData(options.id)
  208 + }
  209 + } catch (error) {
  210 + console.error('页面初始化失败:', error)
  211 + uni.showToast({
  212 + title: '页面初始化失败',
  213 + icon: 'none'
  214 + })
  215 + }
  216 + },
  217 +
  218 + // 设置默认值
  219 + setDefaultValues() {
  220 + // 申请人:当前登录用户
  221 + if (this.userInfo && this.userInfo.userId) {
  222 + this.formData.applicationUserId = this.userInfo.userId
  223 + this.formData.applicationUserName = this.userInfo.realName || this.userInfo.userName || '当前用户'
  224 + }
  225 +
  226 + // 申请门店:当前登录用户的门店
  227 + if (this.newuserInfo && this.newuserInfo.mdid) {
  228 + this.formData.applicationStoreId = this.newuserInfo.mdid
  229 + this.formData.applicationStoreName = this.newuserInfo.mdmc || '当前门店'
  230 + } else if (this.userInfo && this.userInfo.mdid) {
  231 + this.formData.applicationStoreId = this.userInfo.mdid
  232 + this.formData.applicationStoreName = this.userInfo.mdmc || '当前门店'
  233 + }
  234 +
  235 + // 申请时间:默认今日
  236 + const today = new Date()
  237 + today.setHours(0, 0, 0, 0)
  238 + this.formData.applicationTime = today.getTime()
  239 + this.formData.applicationTimeStr = today.toISOString().split('T')[0]
  240 + },
  241 +
  242 + // 申请时间变化
  243 + onApplicationTimeChange(e) {
  244 + this.formData.applicationTimeStr = e.detail.value
  245 + const date = new Date(e.detail.value)
  246 + date.setHours(0, 0, 0, 0)
  247 + this.formData.applicationTime = date.getTime()
  248 + },
  249 +
  250 + // 打开购买物品选择弹窗
  251 + openPurchaseDialog() {
  252 + this.showPurchaseDialog = true
  253 + this.tempSelectedPurchaseIds = this.selectedPurchaseRecords.map(item => item.id)
  254 + this.searchKeyword = ''
  255 + this.loadPurchaseRecords()
  256 + },
  257 +
  258 + // 关闭购买物品选择弹窗
  259 + closePurchaseDialog() {
  260 + this.showPurchaseDialog = false
  261 + this.searchKeyword = ''
  262 + },
  263 +
  264 + // 加载购买记录(未报销的)
  265 + async loadPurchaseRecords() {
  266 + try {
  267 + this.purchaseListLoading = true
  268 +
  269 + const params = {
  270 + createUser:this.newuserInfo&&this.newuserInfo.id?this.newuserInfo.id:'暂无',
  271 + // createUserStoreId: this.formData.applicationStoreId || '暂无',
  272 + approveStatus: '未审批'
  273 + }
  274 +
  275 + const res = await api.getUnreimbursedPurchaseList(params)
  276 + console.error(res)
  277 + if (res.code === 200 && res.data) {
  278 + // 过滤出未报销的记录(ApplicationId为空或未关联)
  279 + // this.purchaseList = (res.data || []).filter(item => !item.applicationId)
  280 + this.purchaseList = res.data || []
  281 + this.filteredPurchaseList = this.purchaseList
  282 + } else {
  283 + this.purchaseList = []
  284 + this.filteredPurchaseList = []
  285 + }
  286 + console.error(this.purchaseList)
  287 + } catch (error) {
  288 + console.error('加载购买记录失败:', error)
  289 + uni.showToast({
  290 + title: '加载购买记录失败',
  291 + icon: 'none'
  292 + })
  293 + this.purchaseList = []
  294 + this.filteredPurchaseList = []
  295 + } finally {
  296 + this.purchaseListLoading = false
  297 + }
  298 + },
  299 +
  300 + // 加载更多购买记录(如果需要分页)
  301 + loadMorePurchaseRecords() {
  302 + // 当前使用不分页接口,暂不需要
  303 + },
  304 +
  305 + // 搜索购买记录
  306 + handleSearch() {
  307 + if (!this.searchKeyword.trim()) {
  308 + this.filteredPurchaseList = this.purchaseList
  309 + return
  310 + }
  311 + const keyword = this.searchKeyword.toLowerCase()
  312 + this.filteredPurchaseList = this.purchaseList.filter(item => {
  313 + const categoryName = (item.reimbursementCategoryName || '').toLowerCase()
  314 + return categoryName.includes(keyword)
  315 + })
  316 + },
  317 +
  318 + // 切换购买记录选择状态
  319 + togglePurchaseSelection(item) {
  320 + const index = this.tempSelectedPurchaseIds.indexOf(item.id)
  321 + if (index > -1) {
  322 + this.tempSelectedPurchaseIds.splice(index, 1)
  323 + } else {
  324 + this.tempSelectedPurchaseIds.push(item.id)
  325 + }
  326 + },
  327 +
  328 + // 判断购买记录是否已选中
  329 + isPurchaseSelected(id) {
  330 + return this.tempSelectedPurchaseIds.indexOf(id) > -1
  331 + },
  332 +
  333 + // 确认选择购买物品
  334 + confirmPurchaseSelection() {
  335 + // 根据选中的ID找到对应的记录
  336 + const selectedRecords = this.purchaseList.filter(item =>
  337 + this.tempSelectedPurchaseIds.indexOf(item.id) > -1
  338 + )
  339 +
  340 + // 去重:移除已存在的记录
  341 + const existingIds = new Set(this.selectedPurchaseRecords.map(item => item.id))
  342 + const newRecords = selectedRecords.filter(item => !existingIds.has(item.id))
  343 +
  344 + // 追加新选中的记录
  345 + this.selectedPurchaseRecords = [...this.selectedPurchaseRecords, ...newRecords]
  346 +
  347 + // 更新ID列表
  348 + this.formData.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(item => item.id)
  349 +
  350 + // 自动计算总金额
  351 + this.calculateTotalAmount()
  352 +
  353 + this.closePurchaseDialog()
  354 + },
  355 +
  356 + // 移除已选中的购买记录
  357 + removePurchaseRecord(index) {
  358 + this.selectedPurchaseRecords.splice(index, 1)
  359 + this.formData.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(item => item.id)
  360 + this.calculateTotalAmount()
  361 + },
  362 +
  363 + // 计算总金额
  364 + calculateTotalAmount() {
  365 + const totalAmount = this.selectedPurchaseRecords.reduce((sum, item) => {
  366 + return sum + (parseFloat(item.amount) || 0)
  367 + }, 0)
  368 + this.formData.amount = totalAmount.toFixed(2)
  369 + },
  370 +
  371 + // 加载编辑数据
  372 + async loadEditData(id) {
  373 + try {
  374 + uni.showLoading({
  375 + title: '加载中...'
  376 + })
  377 +
  378 + const res = await api.getReimbursementDetail(id)
  379 +
  380 + if (res.code === 200 && res.data) {
  381 + const data = res.data
  382 + this.formData = {
  383 + id: data.id,
  384 + applicationUserId: data.applicationUserId,
  385 + applicationUserName: data.applicationUserName,
  386 + applicationStoreId: data.applicationStoreId,
  387 + applicationStoreName: data.applicationStoreName,
  388 + applicationTime: data.applicationTime,
  389 + applicationTimeStr: data.applicationTime ? new Date(data.applicationTime).toISOString().split('T')[0] : undefined,
  390 + amount: data.amount,
  391 + approveStatus: data.approveStatus || '未审批',
  392 + selectedPurchaseRecordIds: []
  393 + }
  394 +
  395 + // 加载已关联的购买记录
  396 + if (data.purchaseRecordsId) {
  397 + await this.loadSelectedPurchaseRecords(data.purchaseRecordsId)
  398 + }
  399 + } else {
  400 + throw new Error(res.message || '加载数据失败')
  401 + }
  402 + } catch (error) {
  403 + console.error('加载编辑数据失败:', error)
  404 + uni.showToast({
  405 + title: '加载数据失败',
  406 + icon: 'none'
  407 + })
  408 + } finally {
  409 + uni.hideLoading()
  410 + }
  411 + },
  412 +
  413 + // 加载已选中的购买记录(编辑时使用)
  414 + async loadSelectedPurchaseRecords(purchaseRecordsId) {
  415 + if (!purchaseRecordsId) {
  416 + this.selectedPurchaseRecords = []
  417 + this.formData.selectedPurchaseRecordIds = []
  418 + return
  419 + }
  420 +
  421 + // 处理逗号分隔的ID字符串
  422 + const normalizedStr = purchaseRecordsId.toString().replace(/[\r\n]+/g, ',').replace(/\s+/g, '')
  423 + const ids = normalizedStr.split(',').map(id => id.trim()).filter(id => id && id !== '')
  424 +
  425 + if (ids.length === 0) {
  426 + this.selectedPurchaseRecords = []
  427 + this.formData.selectedPurchaseRecordIds = []
  428 + return
  429 + }
  430 +
  431 + try {
  432 + // 批量加载购买记录
  433 + const promises = ids.map(id => {
  434 + return purchaseApi.getPurchaseDetail(id)
  435 + .then(res => res.code === 200 ? res.data : null)
  436 + .catch(() => null)
  437 + })
  438 +
  439 + const results = await Promise.all(promises)
  440 + this.selectedPurchaseRecords = results.filter(item => item !== null)
  441 + this.formData.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(item => item.id)
  442 +
  443 + // 计算总金额
  444 + this.calculateTotalAmount()
  445 + } catch (error) {
  446 + console.error('加载购买记录失败:', error)
  447 + uni.showToast({
  448 + title: '加载购买记录失败',
  449 + icon: 'none'
  450 + })
  451 + }
  452 + },
  453 +
  454 + // 表单提交
  455 + async handleFormSubmit() {
  456 + // 验证必填项
  457 + if (!this.formData.selectedPurchaseRecordIds || this.formData.selectedPurchaseRecordIds.length === 0) {
  458 + uni.showToast({
  459 + title: '请至少选择一条购买记录',
  460 + icon: 'none'
  461 + })
  462 + return
  463 + }
  464 +
  465 + if (this.isSubmitting) return
  466 +
  467 + try {
  468 + this.isSubmitting = true
  469 + uni.showLoading({
  470 + title: '提交中...'
  471 + })
  472 +
  473 + // 设置关联购买编号(多个ID用逗号分隔)
  474 + const submitData = {
  475 + ...this.formData,
  476 + purchaseRecordsId: this.formData.selectedPurchaseRecordIds.join(',')
  477 + }
  478 +
  479 + let res
  480 + if (this.formData.id) {
  481 + // 更新
  482 + res = await api.updateReimbursement(this.formData.id, submitData)
  483 + } else {
  484 + // 创建
  485 + res = await api.createReimbursement(submitData)
  486 + }
  487 +
  488 + if (res.code === 200) {
  489 + uni.showToast({
  490 + title: res.message || '操作成功',
  491 + icon: 'success'
  492 + })
  493 + setTimeout(() => {
  494 + uni.navigateBack()
  495 + }, 1500)
  496 + } else {
  497 + throw new Error(res.message || '操作失败')
  498 + }
  499 + } catch (error) {
  500 + console.error('提交失败:', error)
  501 + uni.showToast({
  502 + title: error.message || '提交失败,请重试',
  503 + icon: 'none'
  504 + })
  505 + } finally {
  506 + this.isSubmitting = false
  507 + uni.hideLoading()
  508 + }
  509 + },
  510 +
  511 + // 格式化时间
  512 + formatTime(timestamp) {
  513 + if (!timestamp) return '无'
  514 + const date = new Date(timestamp)
  515 + return date.toLocaleString('zh-CN', {
  516 + year: 'numeric',
  517 + month: '2-digit',
  518 + day: '2-digit',
  519 + hour: '2-digit',
  520 + minute: '2-digit'
  521 + })
  522 + }
  523 + }
  524 + }
  525 +</script>
  526 +
  527 +<style lang="scss" scoped>
  528 + .form-container {
  529 + min-height: 100vh;
  530 + background: linear-gradient(135deg, #e8f5e9 0%, #b2dfdb 100%);
  531 + padding: 40rpx;
  532 + box-sizing: border-box;
  533 + }
  534 +
  535 + .form-card {
  536 + background: #fff;
  537 + border-radius: 32rpx;
  538 + box-shadow: 0 8rpx 32rpx rgba(76, 175, 80, 0.1);
  539 + overflow: hidden;
  540 + }
  541 +
  542 + .form-content {
  543 + padding: 40rpx;
  544 + }
  545 +
  546 + .form-group {
  547 + margin-bottom: 32rpx;
  548 + }
  549 +
  550 + .form-label {
  551 + display: block;
  552 + font-size: 28rpx;
  553 + color: #2e7d32;
  554 + font-weight: 600;
  555 + margin-bottom: 16rpx;
  556 + }
  557 +
  558 + .input-wrapper {
  559 + width: 100%;
  560 + }
  561 +
  562 + .select-input {
  563 + background: #f9fff9;
  564 + border: 3rpx solid #c8e6c9;
  565 + border-radius: 24rpx;
  566 + padding: 24rpx 32rpx;
  567 + }
  568 +
  569 + .custom-select {
  570 + display: flex;
  571 + align-items: center;
  572 + justify-content: space-between;
  573 + background: #f9fff9;
  574 + border: 3rpx solid #c8e6c9;
  575 + border-radius: 24rpx;
  576 + padding: 24rpx 32rpx;
  577 + min-height: 80rpx;
  578 + box-sizing: border-box;
  579 + }
  580 +
  581 + .select-text {
  582 + font-size: 28rpx;
  583 + color: #2e7d32;
  584 + flex: 1;
  585 + }
  586 +
  587 + .select-arrow {
  588 + font-size: 24rpx;
  589 + color: #6a9c6a;
  590 + }
  591 +
  592 + .purchase-select-btn {
  593 + display: flex;
  594 + align-items: center;
  595 + justify-content: center;
  596 + gap: 12rpx;
  597 + padding: 24rpx;
  598 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  599 + border-radius: 24rpx;
  600 + box-shadow: 0 4rpx 12rpx rgba(67, 233, 123, 0.3);
  601 + }
  602 +
  603 + .btn-icon {
  604 + font-size: 32rpx;
  605 + color: #fff;
  606 + }
  607 +
  608 + .btn-text {
  609 + font-size: 28rpx;
  610 + color: #fff;
  611 + font-weight: 600;
  612 + }
  613 +
  614 + .selected-count {
  615 + display: block;
  616 + margin-top: 16rpx;
  617 + font-size: 24rpx;
  618 + color: #6a9c6a;
  619 + text-align: center;
  620 + }
  621 +
  622 + .purchase-list {
  623 + display: flex;
  624 + flex-direction: column;
  625 + gap: 16rpx;
  626 + }
  627 +
  628 + .purchase-item {
  629 + background: #f9fff9;
  630 + border: 2rpx solid #c8e6c9;
  631 + border-radius: 16rpx;
  632 + padding: 24rpx;
  633 + }
  634 +
  635 + .purchase-item-header {
  636 + display: flex;
  637 + justify-content: space-between;
  638 + align-items: center;
  639 + margin-bottom: 12rpx;
  640 + }
  641 +
  642 + .purchase-category {
  643 + font-size: 28rpx;
  644 + color: #2e7d32;
  645 + font-weight: 600;
  646 + }
  647 +
  648 + .purchase-remove {
  649 + width: 48rpx;
  650 + height: 48rpx;
  651 + display: flex;
  652 + align-items: center;
  653 + justify-content: center;
  654 + background: #ffebee;
  655 + border-radius: 50%;
  656 + }
  657 +
  658 + .remove-icon {
  659 + font-size: 24rpx;
  660 + color: #c62828;
  661 + font-weight: 600;
  662 + }
  663 +
  664 + .purchase-item-details {
  665 + display: flex;
  666 + gap: 24rpx;
  667 + margin-bottom: 12rpx;
  668 + }
  669 +
  670 + .purchase-detail {
  671 + font-size: 24rpx;
  672 + color: #6a9c6a;
  673 + }
  674 +
  675 + .purchase-detail.amount {
  676 + color: #f57c00;
  677 + font-weight: 600;
  678 + }
  679 +
  680 + .purchase-item-time {
  681 + font-size: 22rpx;
  682 + color: #909399;
  683 + }
  684 +
  685 + .purchase-time {
  686 + font-size: 22rpx;
  687 + color: #909399;
  688 + }
  689 +
  690 + .btn-group {
  691 + margin-top: 48rpx;
  692 + }
  693 +
  694 + .btn {
  695 + width: 100%;
  696 + padding: 32rpx;
  697 + border-radius: 48rpx;
  698 + font-size: 32rpx;
  699 + font-weight: 600;
  700 + letter-spacing: 2rpx;
  701 + border: none;
  702 + }
  703 +
  704 + .btn-primary {
  705 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  706 + color: #fff;
  707 + box-shadow: 0 8rpx 24rpx rgba(67, 233, 123, 0.3);
  708 + padding: 0;
  709 + }
  710 +
  711 + .btn-primary:active {
  712 + transform: scale(0.98);
  713 + box-shadow: 0 4rpx 12rpx rgba(67, 233, 123, 0.4);
  714 + }
  715 +
  716 + /* 弹窗样式 */
  717 + .modal-overlay {
  718 + position: fixed;
  719 + top: 0;
  720 + left: 0;
  721 + right: 0;
  722 + bottom: 0;
  723 + background: rgba(0, 0, 0, 0.5);
  724 + display: flex;
  725 + align-items: center;
  726 + justify-content: center;
  727 + z-index: 1000;
  728 + }
  729 +
  730 + .modal-content {
  731 + width: 90%;
  732 + max-height: 80vh;
  733 + background: #fff;
  734 + border-radius: 24rpx;
  735 + display: flex;
  736 + flex-direction: column;
  737 + overflow: hidden;
  738 + }
  739 +
  740 + .modal-header {
  741 + display: flex;
  742 + justify-content: space-between;
  743 + align-items: center;
  744 + padding: 32rpx;
  745 + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%);
  746 + }
  747 +
  748 + .modal-title {
  749 + font-size: 32rpx;
  750 + color: #fff;
  751 + font-weight: 600;
  752 + }
  753 +
  754 + .modal-close {
  755 + width: 48rpx;
  756 + height: 48rpx;
  757 + display: flex;
  758 + align-items: center;
  759 + justify-content: center;
  760 + background: rgba(255, 255, 255, 0.3);
  761 + border-radius: 50%;
  762 + }
  763 +
  764 + .close-icon {
  765 + font-size: 28rpx;
  766 + color: #fff;
  767 + font-weight: 600;
  768 + }
  769 +
  770 + .modal-body {
  771 + flex: 1;
  772 + padding: 32rpx;
  773 + overflow: hidden;
  774 + display: flex;
  775 + flex-direction: column;
  776 + }
  777 +
  778 + .search-box {
  779 + margin-bottom: 24rpx;
  780 + }
  781 +
  782 + .search-input {
  783 + background: #f5f5f5;
  784 + border-radius: 16rpx;
  785 + }
  786 +
  787 + .purchase-dialog-list {
  788 + flex: 1;
  789 + max-height: 600rpx;
  790 + }
  791 +
  792 + .loading-text,
  793 + .empty-text {
  794 + text-align: center;
  795 + padding: 80rpx 40rpx;
  796 + color: #909399;
  797 + font-size: 28rpx;
  798 + }
  799 +
  800 + .purchase-dialog-item {
  801 + background: #f9fff9;
  802 + border: 2rpx solid #c8e6c9;
  803 + border-radius: 16rpx;
  804 + padding: 24rpx;
  805 + margin-bottom: 16rpx;
  806 + transition: all 0.2s ease;
  807 + }
  808 +
  809 + .purchase-dialog-item.selected {
  810 + background: #e8f5e9;
  811 + border-color: #43e97b;
  812 + }
  813 +
  814 + .purchase-dialog-header {
  815 + display: flex;
  816 + justify-content: space-between;
  817 + align-items: center;
  818 + margin-bottom: 12rpx;
  819 + }
  820 +
  821 + .purchase-dialog-category {
  822 + font-size: 28rpx;
  823 + color: #2e7d32;
  824 + font-weight: 600;
  825 + }
  826 +
  827 + .selected-mark {
  828 + width: 40rpx;
  829 + height: 40rpx;
  830 + display: flex;
  831 + align-items: center;
  832 + justify-content: center;
  833 + background: #43e97b;
  834 + border-radius: 50%;
  835 + color: #fff;
  836 + font-size: 24rpx;
  837 + font-weight: 600;
  838 + }
  839 +
  840 + .purchase-dialog-details {
  841 + display: flex;
  842 + gap: 24rpx;
  843 + margin-bottom: 12rpx;
  844 + }
  845 +
  846 + .purchase-dialog-detail {
  847 + font-size: 24rpx;
  848 + color: #6a9c6a;
  849 + }
  850 +
  851 + .purchase-dialog-detail.amount {
  852 + color: #f57c00;
  853 + font-weight: 600;
  854 + }
  855 +
  856 + .purchase-dialog-time {
  857 + font-size: 22rpx;
  858 + color: #909399;
  859 + }
  860 +
  861 + .modal-footer {
  862 + display: flex;
  863 + gap: 24rpx;
  864 + padding: 32rpx;
  865 + border-top: 2rpx solid #f0f0f0;
  866 + }
  867 +
  868 + .modal-btn {
  869 + flex: 1;
  870 + padding:15rpx 24rpx;
  871 + border-radius: 24rpx;
  872 + font-size: 28rpx;
  873 + font-weight: 600;
  874 + border: none;
  875 + }
  876 +
  877 + .cancel-btn {
  878 + background: #f5f5f5;
  879 + color: #909399;
  880 + }
  881 +
  882 + .confirm-btn {
  883 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  884 + color: #fff;
  885 + }
  886 +</style>
  887 +
... ...
绿纤uni-app/pages/reimbursement-form/reimbursement-form.vue
... ... @@ -62,8 +62,11 @@
62 62 <view v-for="(item, index) in selectedPurchaseRecords" :key="item.id || index" class="purchase-item">
63 63 <view class="purchase-item-header">
64 64 <text class="purchase-category">{{ item.reimbursementCategoryName || '无' }}</text>
65   - <view class="purchase-remove" @tap="removePurchaseRecord(index)">
66   - <text class="remove-icon">✕</text>
  65 + <view class="purchase-item-actions">
  66 + <text v-if="item.isInitial" class="initial-mark">初始记录</text>
  67 + <view v-if="!item.isInitial" class="purchase-remove" @tap="removePurchaseRecord(index)">
  68 + <text class="remove-icon">✕</text>
  69 + </view>
67 70 </view>
68 71 </view>
69 72 <view class="purchase-item-details">
... ... @@ -78,6 +81,54 @@
78 81 </view>
79 82 </view>
80 83  
  84 + <!-- 审批节点配置(仅新建时显示) -->
  85 + <view class="form-group" v-if="!formData.id">
  86 + <view class="nodes-header">
  87 + <text class="form-label">审批节点配置(1-5个节点)</text>
  88 + <view class="add-node-btn" @tap="addNode" v-if="formData.nodes.length < 5">
  89 + <text class="add-node-text">+ 添加节点</text>
  90 + </view>
  91 + </view>
  92 + <view class="nodes-list">
  93 + <view v-for="(node, index) in formData.nodes" :key="index" class="node-item">
  94 + <view class="node-header">
  95 + <text class="node-order">节点 {{ node.nodeOrder }}</text>
  96 + <view v-if="formData.nodes.length > 1" class="remove-node-btn" @tap="removeNode(index)">
  97 + <text class="remove-node-text">删除</text>
  98 + </view>
  99 + </view>
  100 + <view class="node-form">
  101 + <view class="node-form-item">
  102 + <text class="node-label">节点名称</text>
  103 + <u-input v-model="node.nodeName" placeholder="请输入节点名称" class="node-input" />
  104 + </view>
  105 + <view class="node-form-item">
  106 + <text class="node-label">审批类型</text>
  107 + <picker mode="selector" :range="['会签', '或签']" :value="node.approvalType === '会签' ? 0 : (node.approvalType === '或签' ? 1 : -1)" @change="(e) => { node.approvalType = ['会签', '或签'][e.detail.value] }">
  108 + <view class="custom-select">
  109 + <text class="select-text">{{ node.approvalType || '请选择审批类型' }}</text>
  110 + <text class="select-arrow">▼</text>
  111 + </view>
  112 + </picker>
  113 + </view>
  114 + <view class="node-form-item">
  115 + <text class="node-label">审批人</text>
  116 + <view class="approver-select-wrapper">
  117 + <view class="approver-select-btn" @tap="openUserDialog(index)">
  118 + <text class="approver-btn-text">{{ getApproverDisplayText(node) }}</text>
  119 + </view>
  120 + <view v-if="node.approverNames && node.approverNames.length > 0" class="approver-tags">
  121 + <view v-for="(name, nameIndex) in node.approverNames" :key="nameIndex" class="approver-tag">
  122 + {{ name }}
  123 + </view>
  124 + </view>
  125 + </view>
  126 + </view>
  127 + </view>
  128 + </view>
  129 + </view>
  130 + </view>
  131 +
81 132 <!-- 提交按钮 -->
82 133 <view class="btn-group">
83 134 <button type="submit" class="btn btn-primary"
... ... @@ -107,8 +158,8 @@
107 158 </view>
108 159 <!-- 购买记录列表 -->
109 160 <scroll-view scroll-y class="purchase-dialog-list" @scrolltolower="loadMorePurchaseRecords">
110   - <view v-if="purchaseListLoading" class="loading-text">加载中...</view>
111   - <view v-else-if="filteredPurchaseList.length === 0" class="empty-text">暂无可用记录</view>
  161 + <view v-if="purchaseListLoading && filteredPurchaseList.length === 0" class="loading-text">加载中...</view>
  162 + <view v-else-if="!purchaseListLoading && filteredPurchaseList.length === 0" class="empty-text">暂无可用记录</view>
112 163 <view v-else>
113 164 <view
114 165 v-for="(item, index) in filteredPurchaseList"
... ... @@ -129,6 +180,7 @@
129 180 <text>{{ formatTime(item.purchaseTime) }}</text>
130 181 </view>
131 182 </view>
  183 + <view v-if="purchaseListLoading && filteredPurchaseList.length > 0" class="loading-more">加载更多...</view>
132 184 </view>
133 185 </scroll-view>
134 186 </view>
... ... @@ -138,12 +190,63 @@
138 190 </view>
139 191 </view>
140 192 </view>
  193 +
  194 + <!-- 选择审批人弹窗 -->
  195 + <view v-if="showUserDialog" class="modal-overlay" @tap="closeUserDialog">
  196 + <view class="modal-content" @tap.stop>
  197 + <view class="modal-header">
  198 + <text class="modal-title">选择审批人</text>
  199 + <view class="modal-close" @tap="closeUserDialog">
  200 + <text class="close-icon">✕</text>
  201 + </view>
  202 + </view>
  203 + <view class="modal-body">
  204 + <!-- 搜索框 -->
  205 + <view class="search-box">
  206 + <u-input v-model="userSearchKeyword" placeholder="搜索用户姓名或账号" @input="handleUserSearch"
  207 + class="search-input" />
  208 + </view>
  209 + <!-- 用户列表 -->
  210 + <scroll-view scroll-y class="user-dialog-list" @scrolltolower="loadMoreUsers">
  211 + <view v-if="userListLoading && userList.length === 0" class="loading-text">加载中...</view>
  212 + <view v-else-if="!userListLoading && userList.length === 0" class="empty-text">暂无用户</view>
  213 + <view v-else>
  214 + <view
  215 + v-for="(item, index) in filteredUserList"
  216 + :key="item.id || index"
  217 + class="user-dialog-item"
  218 + :class="{ 'selected': isUserSelected(item.id) }"
  219 + @tap="toggleUserSelection(item)">
  220 + <view class="user-dialog-header">
  221 + <text class="user-dialog-name">{{ item.fullName || item.realName || item.userName || item.id || '无' }}</text>
  222 + <view v-if="isUserSelected(item.id)" class="selected-mark">✓</view>
  223 + </view>
  224 + <view class="user-dialog-details">
  225 + <text class="user-dialog-detail">账号: {{ item.account || '无' }}</text>
  226 + <text class="user-dialog-detail">ID: {{ item.id || '无' }}</text>
  227 + </view>
  228 + </view>
  229 + <view v-if="userListLoading && filteredUserList.length > 0" class="loading-more">加载更多...</view>
  230 + <view v-else-if="!userListLoading && filteredUserList.length >= userTotal && userTotal > 0" class="no-more">没有更多了</view>
  231 + <view v-if="userTotal > 0" class="pagination-info">
  232 + <text>共 {{ userTotal }} 人,已加载 {{ filteredUserList.length }} 人</text>
  233 + </view>
  234 + </view>
  235 + </scroll-view>
  236 + </view>
  237 + <view class="modal-footer">
  238 + <button class="modal-btn cancel-btn" @tap="closeUserDialog">取消</button>
  239 + <button class="modal-btn confirm-btn" @tap="confirmUserSelection">确定</button>
  240 + </view>
  241 + </view>
  242 + </view>
141 243 </view>
142 244 </template>
143 245  
144 246 <script>
145 247 import api from '@/apis/index.js'
146 248 import purchaseApi from '@/apis/modules/purchase.js'
  249 + import oauthApi from '@/apis/modules/oauth.js'
147 250  
148 251 export default {
149 252 data() {
... ... @@ -159,18 +262,40 @@
159 262 applicationTimeStr: undefined,
160 263 amount: undefined,
161 264 approveStatus: '未审批',
162   - selectedPurchaseRecordIds: []
  265 + selectedPurchaseRecordIds: [],
  266 + nodes: [] // 审批节点配置
163 267 },
164 268 selectedPurchaseRecords: [],
  269 + initialPurchaseRecordIds: [], // 存储初始的购买记录ID列表(编辑时使用)
165 270 // 购买物品选择弹窗相关
166 271 showPurchaseDialog: false,
167 272 purchaseList: [],
168 273 filteredPurchaseList: [],
169 274 purchaseListLoading: false,
  275 + purchaseQuery: {
  276 + currentPage: 1,
  277 + pageSize: 20,
  278 + reimbursementCategoryName: ''
  279 + },
  280 + purchaseTotal: 0,
170 281 tempSelectedPurchaseIds: [],
171 282 searchKeyword: '',
172 283 userInfo: null,
173   - newuserInfo: null
  284 + newuserInfo: null,
  285 + // 用户选择弹窗相关
  286 + showUserDialog: false,
  287 + currentNodeIndex: -1, // 当前正在编辑的节点索引
  288 + userList: [],
  289 + filteredUserList: [],
  290 + userListLoading: false,
  291 + userQuery: {
  292 + currentPage: 1,
  293 + pageSize: 20,
  294 + keyword: ''
  295 + },
  296 + userTotal: 0,
  297 + tempSelectedUserIds: [], // 临时选中的用户ID列表
  298 + searchTimer: null // 搜索防抖定时器
174 299 }
175 300 },
176 301  
... ... @@ -237,6 +362,317 @@
237 362 today.setHours(0, 0, 0, 0)
238 363 this.formData.applicationTime = today.getTime()
239 364 this.formData.applicationTimeStr = today.toISOString().split('T')[0]
  365 +
  366 + // 初始化审批节点(仅新建时)
  367 + if (!this.formData.id) {
  368 + this.initNodes()
  369 + }
  370 + },
  371 +
  372 + // 初始化审批节点(新建时默认1个节点)
  373 + initNodes() {
  374 + this.formData.nodes = [
  375 + {
  376 + nodeOrder: 1,
  377 + nodeName: '',
  378 + approvalType: '',
  379 + approverIds: '',
  380 + approverNames: []
  381 + }
  382 + ]
  383 + },
  384 +
  385 + // 添加节点
  386 + addNode() {
  387 + if (this.formData.nodes.length >= 5) {
  388 + uni.showToast({
  389 + title: '最多只能添加5个节点',
  390 + icon: 'none'
  391 + })
  392 + return
  393 + }
  394 + const nextOrder = this.formData.nodes.length + 1
  395 + this.formData.nodes.push({
  396 + nodeOrder: nextOrder,
  397 + nodeName: '',
  398 + approvalType: '',
  399 + approverIds: '',
  400 + approverNames: []
  401 + })
  402 + },
  403 +
  404 + // 删除节点
  405 + removeNode(index) {
  406 + if (this.formData.nodes.length <= 1) {
  407 + uni.showToast({
  408 + title: '至少需要1个节点',
  409 + icon: 'none'
  410 + })
  411 + return
  412 + }
  413 + this.formData.nodes.splice(index, 1)
  414 + // 重新排序
  415 + this.formData.nodes.forEach((node, idx) => {
  416 + node.nodeOrder = idx + 1
  417 + })
  418 + },
  419 +
  420 + // 打开用户选择弹窗
  421 + openUserDialog(nodeIndex) {
  422 + this.currentNodeIndex = nodeIndex
  423 + this.showUserDialog = true
  424 + const node = this.formData.nodes[nodeIndex]
  425 + // 初始化已选中的用户ID
  426 + if (node.approverIds) {
  427 + const idsArray = typeof node.approverIds === 'string'
  428 + ? node.approverIds.split(',').filter(id => id && id.trim())
  429 + : (Array.isArray(node.approverIds) ? node.approverIds : [])
  430 + this.tempSelectedUserIds = [...idsArray]
  431 + } else {
  432 + this.tempSelectedUserIds = []
  433 + }
  434 + this.userSearchKeyword = ''
  435 + this.userQuery.currentPage = 1
  436 + this.userList = []
  437 + this.filteredUserList = []
  438 + this.loadUsers()
  439 + },
  440 +
  441 + // 关闭用户选择弹窗
  442 + closeUserDialog() {
  443 + this.showUserDialog = false
  444 + this.currentNodeIndex = -1
  445 + this.userSearchKeyword = ''
  446 + this.tempSelectedUserIds = []
  447 + // 清除搜索定时器
  448 + if (this.searchTimer) {
  449 + clearTimeout(this.searchTimer)
  450 + this.searchTimer = null
  451 + }
  452 + },
  453 +
  454 + // 加载用户列表
  455 + async loadUsers() {
  456 + try {
  457 + this.userListLoading = true
  458 +
  459 + const params = {
  460 + currentPage: this.userQuery.currentPage,
  461 + pageSize: this.userQuery.pageSize
  462 + }
  463 +
  464 + // 如果有搜索关键词,添加keyword参数
  465 + if (this.userSearchKeyword && this.userSearchKeyword.trim()) {
  466 + params.keyword = this.userSearchKeyword.trim()
  467 + }
  468 +
  469 + const res = await oauthApi.getUserList(params)
  470 +
  471 + if (res.code === 200 && res.data) {
  472 + const newList = res.data.list || []
  473 +
  474 + if (this.userQuery.currentPage === 1) {
  475 + // 第一页,替换数据
  476 + this.userList = newList
  477 + this.filteredUserList = newList
  478 + } else {
  479 + // 后续页,追加数据
  480 + this.userList = [...this.userList, ...newList]
  481 + this.filteredUserList = [...this.filteredUserList, ...newList]
  482 + }
  483 +
  484 + // 获取总数,优先使用pagination.Total,如果没有则使用当前列表长度
  485 + const total = res.data.pagination && res.data.pagination.Total
  486 + if (total !== undefined && total !== null) {
  487 + this.userTotal = total
  488 + } else {
  489 + // 如果没有总数信息,根据当前页和返回数据判断
  490 + if (newList.length < this.userQuery.pageSize) {
  491 + // 返回的数据少于每页数量,说明是最后一页
  492 + this.userTotal = this.filteredUserList.length + newList.length
  493 + } else {
  494 + // 可能还有更多数据,暂时设置为当前数量+1(表示可能还有)
  495 + this.userTotal = this.filteredUserList.length + newList.length + 1
  496 + }
  497 + }
  498 + } else {
  499 + if (this.userQuery.currentPage === 1) {
  500 + this.userList = []
  501 + this.filteredUserList = []
  502 + }
  503 + this.userTotal = 0
  504 + }
  505 + } catch (error) {
  506 + console.error('加载用户列表失败:', error)
  507 + uni.showToast({
  508 + title: '加载用户列表失败',
  509 + icon: 'none'
  510 + })
  511 + if (this.userQuery.currentPage === 1) {
  512 + this.userList = []
  513 + this.filteredUserList = []
  514 + }
  515 + this.userTotal = 0
  516 + } finally {
  517 + this.userListLoading = false
  518 + }
  519 + },
  520 +
  521 + // 调用getUserInfoList API
  522 + async loadUserInfoList(userIds) {
  523 + const request = require('@/service/request.js').default
  524 + const config = require('@/common/config.js').default
  525 + try {
  526 + return await request.post(`${config.getApiBaseUrl()}/api/permission/Users/getUserList`, {
  527 + userId: userIds
  528 + })
  529 + } catch (error) {
  530 + console.error('获取用户信息失败:', error)
  531 + return { code: 500, data: { list: [] } }
  532 + }
  533 + },
  534 +
  535 + // 通过用户ID搜索用户
  536 + async searchUserById() {
  537 + if (!this.userSearchKeyword.trim()) {
  538 + uni.showToast({
  539 + title: '请输入用户ID',
  540 + icon: 'none'
  541 + })
  542 + return
  543 + }
  544 +
  545 + try {
  546 + this.userListLoading = true
  547 + const userIds = this.userSearchKeyword.split(',').map(id => id.trim()).filter(id => id)
  548 +
  549 + if (userIds.length === 0) {
  550 + uni.showToast({
  551 + title: '请输入有效的用户ID',
  552 + icon: 'none'
  553 + })
  554 + return
  555 + }
  556 +
  557 + const res = await this.loadUserInfoList(userIds)
  558 + if (res.code === 200 && res.data && res.data.list) {
  559 + // 合并到现有列表,去重
  560 + const existingIds = new Set(this.userList.map(u => u.id))
  561 + const newUsers = res.data.list.filter(u => !existingIds.has(u.id))
  562 + this.userList = [...this.userList, ...newUsers]
  563 + this.filteredUserList = this.userList
  564 + this.userTotal = this.userList.length
  565 +
  566 + // 如果搜索的用户不在列表中,提示
  567 + if (newUsers.length === 0 && res.data.list.length > 0) {
  568 + uni.showToast({
  569 + title: '用户已在列表中',
  570 + icon: 'none'
  571 + })
  572 + }
  573 + } else {
  574 + uni.showToast({
  575 + title: '未找到用户',
  576 + icon: 'none'
  577 + })
  578 + }
  579 + } catch (error) {
  580 + console.error('搜索用户失败:', error)
  581 + uni.showToast({
  582 + title: '搜索用户失败',
  583 + icon: 'none'
  584 + })
  585 + } finally {
  586 + this.userListLoading = false
  587 + }
  588 + },
  589 +
  590 + // 加载更多用户
  591 + loadMoreUsers() {
  592 + if (this.userListLoading) return
  593 + if (this.filteredUserList.length >= this.userTotal && this.userTotal > 0) {
  594 + return
  595 + }
  596 +
  597 + this.userQuery.currentPage++
  598 + this.loadUsers()
  599 + },
  600 +
  601 + // 搜索用户
  602 + handleUserSearch() {
  603 + // 防抖处理,延迟500ms后执行搜索
  604 + clearTimeout(this.searchTimer)
  605 + this.searchTimer = setTimeout(() => {
  606 + this.userQuery.currentPage = 1
  607 + this.userList = []
  608 + this.filteredUserList = []
  609 + this.loadUsers()
  610 + }, 500)
  611 + },
  612 +
  613 + // 切换用户选择状态
  614 + toggleUserSelection(user) {
  615 + const index = this.tempSelectedUserIds.indexOf(user.id)
  616 + if (index > -1) {
  617 + this.tempSelectedUserIds.splice(index, 1)
  618 + } else {
  619 + this.tempSelectedUserIds.push(user.id)
  620 + }
  621 + },
  622 +
  623 + // 判断用户是否已选中
  624 + isUserSelected(userId) {
  625 + return this.tempSelectedUserIds.indexOf(userId) > -1
  626 + },
  627 +
  628 + // 确认选择用户
  629 + async confirmUserSelection() {
  630 + if (this.currentNodeIndex < 0) return
  631 +
  632 + const node = this.formData.nodes[this.currentNodeIndex]
  633 +
  634 + // 保存选中的用户ID(逗号分隔的字符串)
  635 + node.approverIds = this.tempSelectedUserIds.join(',')
  636 +
  637 + // 获取用户名列表
  638 + if (this.tempSelectedUserIds.length > 0) {
  639 + // 从已加载的用户列表中获取用户名
  640 + const selectedUsers = this.userList.filter(user =>
  641 + this.tempSelectedUserIds.indexOf(user.id) > -1
  642 + )
  643 +
  644 + // 如果有些用户不在当前列表中,需要单独获取
  645 + const missingIds = this.tempSelectedUserIds.filter(id =>
  646 + !selectedUsers.find(u => u.id === id)
  647 + )
  648 +
  649 + if (missingIds.length > 0) {
  650 + try {
  651 + const res = await this.loadUserInfoList(missingIds)
  652 + if (res.code === 200 && res.data && res.data.list) {
  653 + selectedUsers.push(...res.data.list)
  654 + }
  655 + } catch (error) {
  656 + console.error('获取用户信息失败:', error)
  657 + }
  658 + }
  659 +
  660 + node.approverNames = selectedUsers.map(user =>
  661 + user.fullName || user.realName || user.userName || user.id
  662 + )
  663 + } else {
  664 + node.approverNames = []
  665 + }
  666 +
  667 + this.closeUserDialog()
  668 + },
  669 +
  670 + // 获取审批人显示文本
  671 + getApproverDisplayText(node) {
  672 + if (node.approverNames && node.approverNames.length > 0) {
  673 + return `已选择 ${node.approverNames.length} 人`
  674 + }
  675 + return '请选择审批人'
240 676 },
241 677  
242 678 // 申请时间变化
... ... @@ -252,6 +688,7 @@
252 688 this.showPurchaseDialog = true
253 689 this.tempSelectedPurchaseIds = this.selectedPurchaseRecords.map(item => item.id)
254 690 this.searchKeyword = ''
  691 + this.purchaseQuery.currentPage = 1
255 692 this.loadPurchaseRecords()
256 693 },
257 694  
... ... @@ -261,58 +698,74 @@
261 698 this.searchKeyword = ''
262 699 },
263 700  
264   - // 加载购买记录(未报销的
  701 + // 加载购买记录(使用分页接口
265 702 async loadPurchaseRecords() {
266 703 try {
267 704 this.purchaseListLoading = true
268 705  
269 706 const params = {
270   - createUser:this.newuserInfo&&this.newuserInfo.id?this.newuserInfo.id:'暂无',
  707 + currentPage: this.purchaseQuery.currentPage,
  708 + pageSize: this.purchaseQuery.pageSize,
  709 + approveStatus: '未审批', // 只选择未审批的购买记录
271 710 // createUserStoreId: this.formData.applicationStoreId || '暂无',
272   - approveStatus: '未审批'
  711 + createUser:this.newuserInfo&&this.newuserInfo.id?this.newuserInfo.id:'暂无',
  712 + }
  713 +
  714 + if (this.searchKeyword) {
  715 + params.reimbursementCategoryName = this.searchKeyword
273 716 }
274 717  
275   - const res = await api.getUnreimbursedPurchaseList(params)
276   - console.error(res)
  718 + const res = await purchaseApi.getPurchaseList(params)
277 719 if (res.code === 200 && res.data) {
278   - // 过滤出未报销的记录(ApplicationId为空或未关联)
279   - // this.purchaseList = (res.data || []).filter(item => !item.applicationId)
280   - this.purchaseList = res.data || []
281   - this.filteredPurchaseList = this.purchaseList
  720 + const newList = res.data.list || []
  721 + if (this.purchaseQuery.currentPage === 1) {
  722 + // 第一页,替换数据
  723 + this.purchaseList = newList
  724 + this.filteredPurchaseList = newList
  725 + } else {
  726 + // 后续页,追加数据
  727 + this.purchaseList = [...this.purchaseList, ...newList]
  728 + this.filteredPurchaseList = [...this.filteredPurchaseList, ...newList]
  729 + }
  730 + this.purchaseTotal = (res.data.pagination && res.data.pagination.Total) || 0
282 731 } else {
283   - this.purchaseList = []
284   - this.filteredPurchaseList = []
  732 + if (this.purchaseQuery.currentPage === 1) {
  733 + this.purchaseList = []
  734 + this.filteredPurchaseList = []
  735 + }
  736 + this.purchaseTotal = 0
285 737 }
286   - console.error(this.purchaseList)
287 738 } catch (error) {
288 739 console.error('加载购买记录失败:', error)
289 740 uni.showToast({
290 741 title: '加载购买记录失败',
291 742 icon: 'none'
292 743 })
293   - this.purchaseList = []
294   - this.filteredPurchaseList = []
  744 + if (this.purchaseQuery.currentPage === 1) {
  745 + this.purchaseList = []
  746 + this.filteredPurchaseList = []
  747 + }
  748 + this.purchaseTotal = 0
295 749 } finally {
296 750 this.purchaseListLoading = false
297 751 }
298 752 },
299 753  
300   - // 加载更多购买记录(如果需要分页)
  754 + // 加载更多购买记录
301 755 loadMorePurchaseRecords() {
302   - // 当前使用不分页接口,暂不需要
  756 + if (this.purchaseListLoading) return
  757 + if (this.filteredPurchaseList.length >= this.purchaseTotal) return
  758 +
  759 + this.purchaseQuery.currentPage++
  760 + this.loadPurchaseRecords()
303 761 },
304 762  
305 763 // 搜索购买记录
306 764 handleSearch() {
307   - if (!this.searchKeyword.trim()) {
308   - this.filteredPurchaseList = this.purchaseList
309   - return
310   - }
311   - const keyword = this.searchKeyword.toLowerCase()
312   - this.filteredPurchaseList = this.purchaseList.filter(item => {
313   - const categoryName = (item.reimbursementCategoryName || '').toLowerCase()
314   - return categoryName.includes(keyword)
315   - })
  765 + this.purchaseQuery.currentPage = 1
  766 + this.purchaseList = []
  767 + this.filteredPurchaseList = []
  768 + this.loadPurchaseRecords()
316 769 },
317 770  
318 771 // 切换购买记录选择状态
... ... @@ -339,7 +792,11 @@
339 792  
340 793 // 去重:移除已存在的记录
341 794 const existingIds = new Set(this.selectedPurchaseRecords.map(item => item.id))
342   - const newRecords = selectedRecords.filter(item => !existingIds.has(item.id))
  795 + const newRecords = selectedRecords.filter(item => !existingIds.has(item.id)).map(record => {
  796 + // 标记新增的记录
  797 + record.isInitial = false
  798 + return record
  799 + })
343 800  
344 801 // 追加新选中的记录
345 802 this.selectedPurchaseRecords = [...this.selectedPurchaseRecords, ...newRecords]
... ... @@ -355,6 +812,15 @@
355 812  
356 813 // 移除已选中的购买记录
357 814 removePurchaseRecord(index) {
  815 + const record = this.selectedPurchaseRecords[index]
  816 + // 检查是否是初始记录,初始记录不允许移除(编辑时)
  817 + if (record && record.isInitial) {
  818 + uni.showToast({
  819 + title: '初始购买记录不能移除',
  820 + icon: 'none'
  821 + })
  822 + return
  823 + }
358 824 this.selectedPurchaseRecords.splice(index, 1)
359 825 this.formData.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(item => item.id)
360 826 this.calculateTotalAmount()
... ... @@ -379,23 +845,49 @@
379 845  
380 846 if (res.code === 200 && res.data) {
381 847 const data = res.data
  848 + const formData = data.form || data
  849 + const purchaseRecords = data.purchaseRecords || []
  850 +
382 851 this.formData = {
383   - id: data.id,
384   - applicationUserId: data.applicationUserId,
385   - applicationUserName: data.applicationUserName,
386   - applicationStoreId: data.applicationStoreId,
387   - applicationStoreName: data.applicationStoreName,
388   - applicationTime: data.applicationTime,
389   - applicationTimeStr: data.applicationTime ? new Date(data.applicationTime).toISOString().split('T')[0] : undefined,
390   - amount: data.amount,
391   - approveStatus: data.approveStatus || '未审批',
392   - selectedPurchaseRecordIds: []
  852 + id: formData.id,
  853 + applicationUserId: formData.applicationUserId,
  854 + applicationUserName: formData.applicationUserName,
  855 + applicationStoreId: formData.applicationStoreId,
  856 + applicationStoreName: formData.applicationStoreName,
  857 + applicationTime: formData.applicationTime,
  858 + applicationTimeStr: formData.applicationTime ? new Date(formData.applicationTime).toISOString().split('T')[0] : undefined,
  859 + amount: formData.amount,
  860 + approveStatus: formData.approveStatus || formData.approvalStatus || '待审批',
  861 + selectedPurchaseRecordIds: [],
  862 + nodes: [] // 编辑时不需要节点配置
393 863 }
394 864  
395   - // 加载已关联的购买记录
396   - if (data.purchaseRecordsId) {
397   - await this.loadSelectedPurchaseRecords(data.purchaseRecordsId)
398   - }
  865 + // 保存初始的购买记录ID列表
  866 + this.initialPurchaseRecordIds = purchaseRecords.map(record => record.id)
  867 +
  868 + // 填充购买记录列表,标记初始记录
  869 + this.selectedPurchaseRecords = purchaseRecords.map(record => {
  870 + // 处理附件字段
  871 + if (record.attachment) {
  872 + try {
  873 + record.attachment = typeof record.attachment === 'string'
  874 + ? JSON.parse(record.attachment)
  875 + : record.attachment
  876 + } catch (e) {
  877 + record.attachment = []
  878 + }
  879 + } else {
  880 + record.attachment = []
  881 + }
  882 + // 标记为初始记录
  883 + record.isInitial = true
  884 + return record
  885 + })
  886 +
  887 + this.formData.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(item => item.id)
  888 +
  889 + // 计算总金额
  890 + this.calculateTotalAmount()
399 891 } else {
400 892 throw new Error(res.message || '加载数据失败')
401 893 }
... ... @@ -410,46 +902,6 @@
410 902 }
411 903 },
412 904  
413   - // 加载已选中的购买记录(编辑时使用)
414   - async loadSelectedPurchaseRecords(purchaseRecordsId) {
415   - if (!purchaseRecordsId) {
416   - this.selectedPurchaseRecords = []
417   - this.formData.selectedPurchaseRecordIds = []
418   - return
419   - }
420   -
421   - // 处理逗号分隔的ID字符串
422   - const normalizedStr = purchaseRecordsId.toString().replace(/[\r\n]+/g, ',').replace(/\s+/g, '')
423   - const ids = normalizedStr.split(',').map(id => id.trim()).filter(id => id && id !== '')
424   -
425   - if (ids.length === 0) {
426   - this.selectedPurchaseRecords = []
427   - this.formData.selectedPurchaseRecordIds = []
428   - return
429   - }
430   -
431   - try {
432   - // 批量加载购买记录
433   - const promises = ids.map(id => {
434   - return purchaseApi.getPurchaseDetail(id)
435   - .then(res => res.code === 200 ? res.data : null)
436   - .catch(() => null)
437   - })
438   -
439   - const results = await Promise.all(promises)
440   - this.selectedPurchaseRecords = results.filter(item => item !== null)
441   - this.formData.selectedPurchaseRecordIds = this.selectedPurchaseRecords.map(item => item.id)
442   -
443   - // 计算总金额
444   - this.calculateTotalAmount()
445   - } catch (error) {
446   - console.error('加载购买记录失败:', error)
447   - uni.showToast({
448   - title: '加载购买记录失败',
449   - icon: 'none'
450   - })
451   - }
452   - },
453 905  
454 906 // 表单提交
455 907 async handleFormSubmit() {
... ... @@ -462,6 +914,52 @@
462 914 return
463 915 }
464 916  
  917 + // 新建时验证节点配置
  918 + if (!this.formData.id) {
  919 + if (!this.formData.nodes || this.formData.nodes.length < 1 || this.formData.nodes.length > 5) {
  920 + uni.showToast({
  921 + title: '审批节点数量必须在1-5个之间',
  922 + icon: 'none'
  923 + })
  924 + return
  925 + }
  926 +
  927 + // 验证每个节点
  928 + for (let i = 0; i < this.formData.nodes.length; i++) {
  929 + const node = this.formData.nodes[i]
  930 + if (!node.nodeName) {
  931 + uni.showToast({
  932 + title: `请填写节点${node.nodeOrder}的名称`,
  933 + icon: 'none'
  934 + })
  935 + return
  936 + }
  937 + if (!node.approvalType) {
  938 + uni.showToast({
  939 + title: `请选择节点${node.nodeOrder}的审批类型`,
  940 + icon: 'none'
  941 + })
  942 + return
  943 + }
  944 + // 验证审批人
  945 + let approverIdsArray = []
  946 + if (node.approverIds) {
  947 + if (Array.isArray(node.approverIds)) {
  948 + approverIdsArray = node.approverIds.filter(id => id)
  949 + } else if (typeof node.approverIds === 'string') {
  950 + approverIdsArray = node.approverIds.split(',').filter(id => id && id.trim())
  951 + }
  952 + }
  953 + if (approverIdsArray.length === 0) {
  954 + uni.showToast({
  955 + title: `请为节点${node.nodeOrder}选择至少一个审批人`,
  956 + icon: 'none'
  957 + })
  958 + return
  959 + }
  960 + }
  961 + }
  962 +
465 963 if (this.isSubmitting) return
466 964  
467 965 try {
... ... @@ -470,31 +968,154 @@
470 968 title: '提交中...'
471 969 })
472 970  
473   - // 设置关联购买编号(多个ID用逗号分隔)
474   - const submitData = {
475   - ...this.formData,
476   - purchaseRecordsId: this.formData.selectedPurchaseRecordIds.join(',')
477   - }
478   -
479   - let res
  971 + let submitData = {}
  972 +
480 973 if (this.formData.id) {
481   - // 更新
482   - res = await api.updateReimbursement(this.formData.id, submitData)
  974 + // 更新:只传递有值的字段
  975 + submitData = {
  976 + id: this.formData.id
  977 + }
  978 + if (this.formData.applicationUserId) {
  979 + submitData.applicationUserId = this.formData.applicationUserId
  980 + }
  981 + if (this.formData.applicationUserName) {
  982 + submitData.applicationUserName = this.formData.applicationUserName
  983 + }
  984 + if (this.formData.applicationStoreId) {
  985 + submitData.applicationStoreId = this.formData.applicationStoreId
  986 + }
  987 + if (this.formData.applicationTime) {
  988 + submitData.applicationTime = this.formData.applicationTime
  989 + }
  990 + if (this.formData.amount) {
  991 + submitData.amount = this.formData.amount
  992 + }
  993 + if (this.formData.selectedPurchaseRecordIds && this.formData.selectedPurchaseRecordIds.length > 0) {
  994 + submitData.selectedPurchaseRecordIds = this.formData.selectedPurchaseRecordIds
  995 + }
  996 +
  997 + const res = await api.updateReimbursement(this.formData.id, submitData)
  998 +
  999 + if (res.code === 200) {
  1000 + uni.showToast({
  1001 + title: '修改成功',
  1002 + icon: 'success'
  1003 + })
  1004 +
  1005 + // 修改成功后,自动重新提交审批
  1006 + try {
  1007 + const submitRes = await api.submitApproval(this.formData.id)
  1008 + if (submitRes.code === 200) {
  1009 + uni.showToast({
  1010 + title: '已重新提交审批',
  1011 + icon: 'success'
  1012 + })
  1013 + } else {
  1014 + uni.showToast({
  1015 + title: '修改成功,但重新提交审批失败',
  1016 + icon: 'none'
  1017 + })
  1018 + }
  1019 + } catch (error) {
  1020 + console.error('重新提交审批失败:', error)
  1021 + uni.showToast({
  1022 + title: '修改成功,但重新提交审批失败,请手动提交',
  1023 + icon: 'none'
  1024 + })
  1025 + }
  1026 +
  1027 + setTimeout(() => {
  1028 + uni.navigateBack()
  1029 + }, 1500)
  1030 + } else {
  1031 + throw new Error(res.message || '修改失败')
  1032 + }
483 1033 } else {
484 1034 // 创建
485   - res = await api.createReimbursement(submitData)
486   - }
487   -
488   - if (res.code === 200) {
489   - uni.showToast({
490   - title: res.message || '操作成功',
491   - icon: 'success'
492   - })
493   - setTimeout(() => {
494   - uni.navigateBack()
495   - }, 1500)
496   - } else {
497   - throw new Error(res.message || '操作失败')
  1035 + submitData = {
  1036 + applicationUserId: this.formData.applicationUserId,
  1037 + applicationUserName: this.formData.applicationUserName,
  1038 + applicationStoreId: this.formData.applicationStoreId,
  1039 + applicationTime: this.formData.applicationTime,
  1040 + amount: this.formData.amount,
  1041 + selectedPurchaseRecordIds: this.formData.selectedPurchaseRecordIds,
  1042 + nodes: this.formData.nodes.map(node => {
  1043 + // 转换审批人ID为数组
  1044 + let approverIds = []
  1045 + if (node.approverIds) {
  1046 + if (Array.isArray(node.approverIds)) {
  1047 + approverIds = node.approverIds.filter(id => id)
  1048 + } else if (typeof node.approverIds === 'string') {
  1049 + approverIds = node.approverIds.split(',').filter(id => id && id.trim())
  1050 + }
  1051 + }
  1052 +
  1053 + return {
  1054 + nodeOrder: node.nodeOrder,
  1055 + nodeName: node.nodeName,
  1056 + approvalType: node.approvalType,
  1057 + approverIds: approverIds,
  1058 + approverNames: node.approverNames && node.approverNames.length > 0
  1059 + ? node.approverNames
  1060 + : approverIds
  1061 + }
  1062 + })
  1063 + }
  1064 +
  1065 + const res = await api.createReimbursement(submitData)
  1066 +
  1067 + if (res.code === 200) {
  1068 + uni.showToast({
  1069 + title: '创建成功',
  1070 + icon: 'success'
  1071 + })
  1072 +
  1073 + // 创建成功后,自动提交审批
  1074 + let applicationId = null
  1075 + if (res.data) {
  1076 + if (typeof res.data === 'object' && res.data.id) {
  1077 + applicationId = res.data.id
  1078 + } else if (typeof res.data === 'string') {
  1079 + applicationId = res.data
  1080 + } else {
  1081 + applicationId = String(res.data)
  1082 + }
  1083 + }
  1084 +
  1085 + if (applicationId) {
  1086 + try {
  1087 + const submitRes = await api.submitApproval(applicationId)
  1088 + if (submitRes.code === 200) {
  1089 + uni.showToast({
  1090 + title: '已自动提交审批',
  1091 + icon: 'success'
  1092 + })
  1093 + } else {
  1094 + uni.showToast({
  1095 + title: '创建成功,但提交审批失败',
  1096 + icon: 'none'
  1097 + })
  1098 + }
  1099 + } catch (error) {
  1100 + console.error('提交审批失败:', error)
  1101 + uni.showToast({
  1102 + title: '创建成功,但提交审批失败,请手动提交',
  1103 + icon: 'none'
  1104 + })
  1105 + }
  1106 + } else {
  1107 + uni.showToast({
  1108 + title: '创建成功,但无法获取申请ID,请手动提交审批',
  1109 + icon: 'none'
  1110 + })
  1111 + }
  1112 +
  1113 + setTimeout(() => {
  1114 + uni.navigateBack()
  1115 + }, 1500)
  1116 + } else {
  1117 + throw new Error(res.message || '创建失败')
  1118 + }
498 1119 }
499 1120 } catch (error) {
500 1121 console.error('提交失败:', error)
... ... @@ -639,6 +1260,20 @@
639 1260 margin-bottom: 12rpx;
640 1261 }
641 1262  
  1263 + .purchase-item-actions {
  1264 + display: flex;
  1265 + align-items: center;
  1266 + gap: 16rpx;
  1267 + }
  1268 +
  1269 + .initial-mark {
  1270 + font-size: 22rpx;
  1271 + color: #909399;
  1272 + padding: 4rpx 12rpx;
  1273 + background: #f5f5f5;
  1274 + border-radius: 8rpx;
  1275 + }
  1276 +
642 1277 .purchase-category {
643 1278 font-size: 28rpx;
644 1279 color: #2e7d32;
... ... @@ -883,5 +1518,203 @@
883 1518 background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
884 1519 color: #fff;
885 1520 }
  1521 +
  1522 + /* 审批节点配置样式 */
  1523 + .nodes-header {
  1524 + display: flex;
  1525 + justify-content: space-between;
  1526 + align-items: center;
  1527 + margin-bottom: 24rpx;
  1528 + }
  1529 +
  1530 + .add-node-btn {
  1531 + padding: 12rpx 24rpx;
  1532 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  1533 + border-radius: 16rpx;
  1534 + }
  1535 +
  1536 + .add-node-text {
  1537 + font-size: 24rpx;
  1538 + color: #fff;
  1539 + font-weight: 600;
  1540 + }
  1541 +
  1542 + .nodes-list {
  1543 + display: flex;
  1544 + flex-direction: column;
  1545 + gap: 24rpx;
  1546 + }
  1547 +
  1548 + .node-item {
  1549 + background: #f9fff9;
  1550 + border: 2rpx solid #c8e6c9;
  1551 + border-radius: 16rpx;
  1552 + padding: 24rpx;
  1553 + }
  1554 +
  1555 + .node-header {
  1556 + display: flex;
  1557 + justify-content: space-between;
  1558 + align-items: center;
  1559 + margin-bottom: 24rpx;
  1560 + padding-bottom: 16rpx;
  1561 + border-bottom: 2rpx solid #e0e0e0;
  1562 + }
  1563 +
  1564 + .node-order {
  1565 + font-size: 28rpx;
  1566 + color: #2e7d32;
  1567 + font-weight: 600;
  1568 + }
  1569 +
  1570 + .remove-node-btn {
  1571 + padding: 8rpx 16rpx;
  1572 + background: #ffebee;
  1573 + border-radius: 8rpx;
  1574 + }
  1575 +
  1576 + .remove-node-text {
  1577 + font-size: 24rpx;
  1578 + color: #c62828;
  1579 + font-weight: 500;
  1580 + }
  1581 +
  1582 + .node-form {
  1583 + display: flex;
  1584 + flex-direction: column;
  1585 + gap: 24rpx;
  1586 + }
  1587 +
  1588 + .node-form-item {
  1589 + display: flex;
  1590 + flex-direction: column;
  1591 + gap: 12rpx;
  1592 + }
  1593 +
  1594 + .node-label {
  1595 + font-size: 26rpx;
  1596 + color: #2e7d32;
  1597 + font-weight: 500;
  1598 + }
  1599 +
  1600 + .node-input {
  1601 + background: #fff;
  1602 + border: 2rpx solid #c8e6c9;
  1603 + border-radius: 16rpx;
  1604 + padding: 20rpx 24rpx;
  1605 + }
  1606 +
  1607 + .node-tip {
  1608 + font-size: 22rpx;
  1609 + color: #909399;
  1610 + margin-top: -8rpx;
  1611 + }
  1612 +
  1613 + .loading-more {
  1614 + text-align: center;
  1615 + padding: 32rpx;
  1616 + color: #909399;
  1617 + font-size: 24rpx;
  1618 + }
  1619 +
  1620 + .no-more {
  1621 + text-align: center;
  1622 + padding: 32rpx;
  1623 + color: #909399;
  1624 + font-size: 24rpx;
  1625 + }
  1626 +
  1627 + .pagination-info {
  1628 + text-align: center;
  1629 + padding: 24rpx;
  1630 + color: #6a9c6a;
  1631 + font-size: 22rpx;
  1632 + background: #f9fff9;
  1633 + border-top: 2rpx solid #e0e0e0;
  1634 + }
  1635 +
  1636 + /* 审批人选择相关样式 */
  1637 + .approver-select-wrapper {
  1638 + display: flex;
  1639 + flex-direction: column;
  1640 + gap: 16rpx;
  1641 + }
  1642 +
  1643 + .approver-select-btn {
  1644 + display: flex;
  1645 + align-items: center;
  1646 + justify-content: space-between;
  1647 + background: #f9fff9;
  1648 + border: 3rpx solid #c8e6c9;
  1649 + border-radius: 24rpx;
  1650 + padding: 24rpx 32rpx;
  1651 + min-height: 80rpx;
  1652 + box-sizing: border-box;
  1653 + }
  1654 +
  1655 + .approver-btn-text {
  1656 + font-size: 28rpx;
  1657 + color: #2e7d32;
  1658 + flex: 1;
  1659 + }
  1660 +
  1661 + .approver-tags {
  1662 + display: flex;
  1663 + flex-wrap: wrap;
  1664 + gap: 12rpx;
  1665 + }
  1666 +
  1667 + .approver-tag {
  1668 + display: inline-block;
  1669 + padding: 8rpx 16rpx;
  1670 + background: #e8f5e9;
  1671 + border: 2rpx solid #c8e6c9;
  1672 + border-radius: 16rpx;
  1673 + font-size: 24rpx;
  1674 + color: #2e7d32;
  1675 + }
  1676 +
  1677 + /* 用户选择弹窗样式 */
  1678 + .user-dialog-list {
  1679 + flex: 1;
  1680 + max-height: 600rpx;
  1681 + }
  1682 +
  1683 + .user-dialog-item {
  1684 + background: #f9fff9;
  1685 + border: 2rpx solid #c8e6c9;
  1686 + border-radius: 16rpx;
  1687 + padding: 24rpx;
  1688 + margin-bottom: 16rpx;
  1689 + transition: all 0.2s ease;
  1690 + }
  1691 +
  1692 + .user-dialog-item.selected {
  1693 + background: #e8f5e9;
  1694 + border-color: #43e97b;
  1695 + }
  1696 +
  1697 + .user-dialog-header {
  1698 + display: flex;
  1699 + justify-content: space-between;
  1700 + align-items: center;
  1701 + margin-bottom: 12rpx;
  1702 + }
  1703 +
  1704 + .user-dialog-name {
  1705 + font-size: 28rpx;
  1706 + color: #2e7d32;
  1707 + font-weight: 600;
  1708 + }
  1709 +
  1710 + .user-dialog-details {
  1711 + display: flex;
  1712 + gap: 24rpx;
  1713 + }
  1714 +
  1715 + .user-dialog-detail {
  1716 + font-size: 24rpx;
  1717 + color: #6a9c6a;
  1718 + }
886 1719 </style>
887 1720  
... ...
绿纤uni-app/pages/reimbursement-list/reimbursement-list - 副本.vue 0 → 100644
  1 +<template>
  2 + <view class="container">
  3 + <!-- 日期范围筛选 -->
  4 + <view class="date-filter-card">
  5 + <view class="date-filter-header">
  6 + <view class="date-range-display" @click="showCalendarFun">
  7 + <text class="date-text">{{ formatDateRange() }}</text>
  8 + <text class="calendar-icon">📅</text>
  9 + </view>
  10 + </view>
  11 + <view class="status-filter">
  12 + <text class="status-filter-label">审批状态</text>
  13 + <picker mode="selector" :range="approveStatusOptions" :range-key="'label'" :value="approveStatusIndex" @change="handleStatusChange">
  14 + <view class="status-picker">
  15 + <text class="status-text">{{ approveStatusOptions[approveStatusIndex].label }}</text>
  16 + <text class="picker-arrow">▼</text>
  17 + </view>
  18 + </picker>
  19 + </view>
  20 + </view>
  21 +
  22 + <!-- 添加按钮 -->
  23 + <view class="add-btn-wrapper">
  24 + <view class="add-btn" @click="goToAdd">
  25 + <text class="add-text">添加</text>
  26 + </view>
  27 + </view>
  28 +
  29 + <!-- 日历组件 -->
  30 + <uni-calendar
  31 + :range="true"
  32 + ref="calendar"
  33 + @confirm="handleDateConfirm"
  34 + :insert="false"
  35 + ></uni-calendar>
  36 +
  37 + <!-- 数据列表 -->
  38 + <view class="list-card">
  39 + <view class="list-header">
  40 + <text class="header-text">报销申请记录</text>
  41 + <text class="total-count">共 {{ totalCount }} 条</text>
  42 + </view>
  43 +
  44 + <scroll-view scroll-y class="list-content" @scrolltolower="loadMore">
  45 + <view v-for="(item, index) in dataList" :key="index" class="list-item" @click.stop="handleItemClick(item)">
  46 + <view class="item-header">
  47 + <view class="applicant-name">{{ item.applicationUserName || '无' }}</view>
  48 + <view class="application-time">{{ formatTime(item.applicationTime) }}</view>
  49 + </view>
  50 + <view class="item-details">
  51 + <!-- <view class="detail-item">
  52 + <text class="detail-label">门店:</text>
  53 + <text class="detail-value">{{ getStoreName(item.applicationStoreId) || '无' }}</text>
  54 + </view> -->
  55 + <view class="detail-item">
  56 + <text class="detail-label">总金额:</text>
  57 + <text class="detail-value amount">¥{{ item.amount || 0 }}</text>
  58 + </view>
  59 + </view>
  60 + <view class="item-footer">
  61 + <view class="status-badge" :class="item.approveStatus=='已审批'?'approved':item.approveStatus=='待审批'?'pending':item.approveStatus=='未通过'?'rejected':'unapproved'">
  62 + {{ item.approveStatus || '未审批' }}
  63 + </view>
  64 + <view class="action-buttons">
  65 + <view class="action-btn view-btn" @click.stop="handleView(item)">
  66 + <text class="btn-text">查看</text>
  67 + </view>
  68 + <view v-if="item.approveStatus === '未审批'" class="action-btn edit-btn" @click.stop="handleEdit(item)">
  69 + <text class="btn-text">编辑</text>
  70 + </view>
  71 + </view>
  72 + </view>
  73 + </view>
  74 +
  75 + <!-- 空状态 -->
  76 + <view v-if="!loading && dataList.length === 0" class="empty-state">
  77 + <view class="empty-icon">💰</view>
  78 + <view>暂无报销申请记录</view>
  79 + </view>
  80 +
  81 + <!-- 加载状态 -->
  82 + <view v-if="loading && dataList.length === 0" class="loading">正在加载数据...</view>
  83 +
  84 + <!-- 加载更多 -->
  85 + <u-loadmore v-if="dataList.length > 0" :status="loadmoreStatus" :load-text="loadText"></u-loadmore>
  86 + </scroll-view>
  87 + </view>
  88 + </view>
  89 +</template>
  90 +
  91 +<script>
  92 + import api from '@/apis/index.js'
  93 + import config from '@/common/config.js'
  94 + import uniCalendar from '@/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue'
  95 +
  96 + export default {
  97 + components: {
  98 + uniCalendar
  99 + },
  100 + data() {
  101 + // 初始化日期范围(本月1号到今天)
  102 + const now = new Date()
  103 + const year = now.getFullYear()
  104 + const month = now.getMonth()
  105 + const firstDay = new Date(year, month, 1)
  106 + const today = new Date(year, now.getMonth(), now.getDate(), 23, 59, 59, 999)
  107 +
  108 + // 格式化日期为 YYYY-MM-DD(使用本地时间,避免时区问题)
  109 + const formatDateLocal = (date) => {
  110 + const y = date.getFullYear()
  111 + const m = String(date.getMonth() + 1).padStart(2, '0')
  112 + const d = String(date.getDate()).padStart(2, '0')
  113 + return `${y}-${m}-${d}`
  114 + }
  115 +
  116 + return {
  117 + loading: false,
  118 + dataList: [],
  119 + currentPage: 1,
  120 + pageSize: 10,
  121 + totalCount: 0,
  122 + hasMore: true,
  123 + loadmoreStatus: 'loadmore',
  124 + loadText: {
  125 + loadmore: '点击或上拉加载更多',
  126 + loading: '正在加载...',
  127 + nomore: '没有更多了'
  128 + },
  129 + userInfo: uni.getStorageSync('userInfo'),
  130 + newuserInfo: uni.getStorageSync('newuserInfo'),
  131 + storeOptions: [],
  132 + // 日期范围筛选相关
  133 + showCalendar: false,
  134 + startDate: formatDateLocal(firstDay),
  135 + endDate: formatDateLocal(now),
  136 + dateRange: [firstDay.getTime(), today.getTime()],
  137 + // 审批状态筛选
  138 + approveStatusIndex: 0,
  139 + approveStatusOptions: [
  140 + { label: '全部', value: '' },
  141 + { label: '未审批', value: '未审批' },
  142 + { label: '待审批', value: '待审批' },
  143 + { label: '已审批', value: '已审批' },
  144 + { label: '未通过', value: '未通过' }
  145 + ]
  146 + }
  147 + },
  148 +
  149 + onLoad() {
  150 + // this.loadStoreOptions()
  151 + this.loadReimbursementList()
  152 + },
  153 +
  154 + methods: {
  155 + // 加载门店列表
  156 + async loadStoreOptions() {
  157 + try {
  158 + const res = await api.getStoreList({
  159 + currentPage: 1,
  160 + pageSize: 1000
  161 + })
  162 + if (res.code === 200 && res.data && res.data.list) {
  163 + this.storeOptions = res.data.list
  164 + } else {
  165 + this.storeOptions = []
  166 + }
  167 + } catch (error) {
  168 + console.error('加载门店列表失败:', error)
  169 + this.storeOptions = []
  170 + }
  171 + },
  172 +
  173 + // 获取门店名称
  174 + getStoreName(storeId) {
  175 + if (!storeId) return '无'
  176 + const store = this.storeOptions.find(item => item.id === storeId)
  177 + return store ? store.dm : '无'
  178 + },
  179 +
  180 + // 显示日历
  181 + showCalendarFun() {
  182 + this.$refs.calendar.open()
  183 + },
  184 +
  185 + // 日期确认
  186 + handleDateConfirm(e) {
  187 + console.log('日期选择结果:', e)
  188 +
  189 + // uni-calendar组件返回的是包含start和end的对象
  190 + if (e && e.range && e.range.data) {
  191 + // 更新dateRange用于API调用(时间戳格式)
  192 + const startTime = new Date(e.range.data[0]).getTime()
  193 + const endTime = new Date(e.range.data[e.range.data.length-1]).getTime()
  194 + this.dateRange = [startTime, endTime]
  195 +
  196 + // 更新startDate和endDate用于显示
  197 + this.startDate = e.range.data[0]
  198 + this.endDate = e.range.data[e.range.data.length-1]
  199 +
  200 + // 日期变化时重新加载数据
  201 + this.currentPage = 1
  202 + this.loadReimbursementList()
  203 + }
  204 + },
  205 +
  206 + // 格式化日期范围显示
  207 + formatDateRange() {
  208 + if (!this.startDate || !this.endDate) {
  209 + return '请选择日期'
  210 + }
  211 +
  212 + // 处理日期字符串,确保正确解析
  213 + const startDate = new Date(this.startDate + 'T00:00:00')
  214 + const endDate = new Date(this.endDate + 'T00:00:00')
  215 +
  216 + const formatDate = (date) => {
  217 + const year = date.getFullYear()
  218 + const month = String(date.getMonth() + 1).padStart(2, '0')
  219 + const day = String(date.getDate()).padStart(2, '0')
  220 + return `${year}/${month}/${day}`
  221 + }
  222 +
  223 + return `${formatDate(startDate)} - ${formatDate(endDate)}`
  224 + },
  225 +
  226 + // 审批状态变化
  227 + handleStatusChange(e) {
  228 + this.approveStatusIndex = e.detail.value
  229 + this.currentPage = 1
  230 + this.loadReimbursementList()
  231 + },
  232 +
  233 + // 加载报销申请列表
  234 + async loadReimbursementList() {
  235 + if (this.loading) return
  236 +
  237 + try {
  238 + this.loading = true
  239 + this.loadmoreStatus = 'loading'
  240 +
  241 + // 构建查询参数
  242 + const params = {
  243 + currentPage: this.currentPage,
  244 + pageSize: this.pageSize
  245 + }
  246 +
  247 + // 添加日期范围筛选
  248 + if (this.startDate && this.endDate) {
  249 + const startTimestamp = new Date(this.startDate + 'T00:00:00').getTime()
  250 + const endTimestamp = new Date(this.endDate + 'T23:59:59').getTime()
  251 + params.applicationTime = `${startTimestamp},${endTimestamp}`
  252 + }
  253 +
  254 + // 添加审批状态筛选
  255 + const selectedStatus = this.approveStatusOptions[this.approveStatusIndex]
  256 + if (selectedStatus && selectedStatus.value) {
  257 + params.approveStatus = selectedStatus.value
  258 + }
  259 +
  260 + // 添加门店筛选(默认当前登录人的门店)
  261 + // params.applicationStoreId = this.newuserInfo&&this.newuserInfo.mdid?this.newuserInfo.mdid:'暂无'
  262 + params.applicationUserId = this.newuserInfo&&this.newuserInfo.id?this.newuserInfo.id:'暂无'
  263 + const res = await api.getReimbursementList(params)
  264 +
  265 + if (res.code === 200 && res.data) {
  266 + const list = res.data.list || []
  267 + const pagination = res.data.pagination || {}
  268 +
  269 + if (this.currentPage === 1) {
  270 + this.dataList = list
  271 + } else {
  272 + this.dataList = [...this.dataList, ...list]
  273 + }
  274 +
  275 + this.totalCount = pagination.total || 0
  276 + this.hasMore = this.dataList.length < this.totalCount
  277 +
  278 + if (!this.hasMore) {
  279 + this.loadmoreStatus = 'nomore'
  280 + } else {
  281 + this.loadmoreStatus = 'loadmore'
  282 + }
  283 + } else {
  284 + uni.showToast({
  285 + title: res.message || '加载失败',
  286 + icon: 'none'
  287 + })
  288 + }
  289 + } catch (error) {
  290 + console.error('加载报销申请列表失败:', error)
  291 + uni.showToast({
  292 + title: '加载失败,请重试',
  293 + icon: 'none'
  294 + })
  295 + } finally {
  296 + this.loading = false
  297 + }
  298 + },
  299 +
  300 + // 加载更多
  301 + async loadMore() {
  302 + if (this.loading || !this.hasMore || this.loadmoreStatus === 'nomore') return
  303 +
  304 + this.loadmoreStatus = 'loading'
  305 + this.currentPage++
  306 + await this.loadReimbursementList()
  307 + },
  308 +
  309 + // 跳转到添加页面
  310 + goToAdd() {
  311 + uni.navigateTo({
  312 + url: '/pages/reimbursement-form/reimbursement-form'
  313 + })
  314 + },
  315 +
  316 + // 查看详情
  317 + handleView(item) {
  318 + uni.navigateTo({
  319 + url: `/pages/reimbursement-detail/reimbursement-detail?id=${item.id}`
  320 + })
  321 + },
  322 +
  323 + // 编辑记录(只有未审批状态才能编辑)
  324 + handleEdit(item) {
  325 + if (item.approveStatus === '未审批') {
  326 + uni.navigateTo({
  327 + url: `/pages/reimbursement-form/reimbursement-form?id=${item.id}`
  328 + })
  329 + } else {
  330 + uni.showToast({
  331 + title: '已审批的记录不能编辑',
  332 + icon: 'none'
  333 + })
  334 + }
  335 + },
  336 +
  337 + // 处理列表项点击(用于编辑)
  338 + handleItemClick(item) {
  339 + // 只有未审批状态才能编辑
  340 + if (item.approveStatus === '未审批') {
  341 + this.handleEdit(item)
  342 + }
  343 + },
  344 +
  345 + // 格式化时间
  346 + formatTime(timestamp) {
  347 + if (!timestamp) return '无'
  348 + const date = new Date(timestamp)
  349 + return date.toLocaleString('zh-CN', {
  350 + year: 'numeric',
  351 + month: '2-digit',
  352 + day: '2-digit',
  353 + hour: '2-digit',
  354 + minute: '2-digit'
  355 + })
  356 + },
  357 +
  358 + // 获取状态样式类
  359 + getStatusClass(status) {
  360 + switch (status) {
  361 + case '已审批':
  362 + return 'approved'
  363 + case '待审批':
  364 + return 'pending'
  365 + case '未通过':
  366 + return 'rejected'
  367 + case '未审批':
  368 + default:
  369 + return 'unapproved'
  370 + }
  371 + }
  372 + }
  373 + }
  374 +</script>
  375 +
  376 +<style lang="scss" scoped>
  377 + .container {
  378 + min-height: 100vh;
  379 + background: linear-gradient(135deg, #e8f5e9 0%, #b2dfdb 100%);
  380 + padding: 40rpx;
  381 + box-sizing: border-box;
  382 + }
  383 +
  384 + .date-filter-card {
  385 + background: #fff;
  386 + border-radius: 24rpx;
  387 + padding: 32rpx;
  388 + margin-bottom: 24rpx;
  389 + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.1);
  390 + }
  391 +
  392 + .date-filter-header {
  393 + margin-bottom: 24rpx;
  394 + }
  395 +
  396 + .date-range-display {
  397 + display: flex;
  398 + align-items: center;
  399 + justify-content: space-between;
  400 + padding: 24rpx;
  401 + background: #f9fff9;
  402 + border: 3rpx solid #c8e6c9;
  403 + border-radius: 16rpx;
  404 + }
  405 +
  406 + .date-text {
  407 + font-size: 28rpx;
  408 + color: #2e7d32;
  409 + font-weight: 500;
  410 + }
  411 +
  412 + .calendar-icon {
  413 + font-size: 32rpx;
  414 + }
  415 +
  416 + .status-filter {
  417 + display: flex;
  418 + align-items: center;
  419 + gap: 24rpx;
  420 + justify-content: space-between;
  421 + }
  422 +
  423 + .status-filter-label {
  424 + font-size: 28rpx;
  425 + color: #2e7d32;
  426 + font-weight: 500;
  427 + }
  428 +
  429 + .status-picker {
  430 + flex: 1;
  431 + display: flex;
  432 + align-items: center;
  433 + justify-content: space-between;
  434 + padding: 24rpx;
  435 + background: #f9fff9;
  436 + border: 3rpx solid #c8e6c9;
  437 + border-radius: 16rpx;
  438 + }
  439 +
  440 + .status-text {
  441 + font-size: 28rpx;
  442 + color: #2e7d32;
  443 + }
  444 +
  445 + .picker-arrow {
  446 + font-size: 24rpx;
  447 + color: #6a9c6a;
  448 + margin-left: 10rpx;
  449 + }
  450 +
  451 + .add-btn-wrapper {
  452 + margin-bottom: 24rpx;
  453 + display: flex;
  454 + justify-content: flex-end;
  455 + }
  456 +
  457 + .add-btn {
  458 + display: flex;
  459 + align-items: center;
  460 + justify-content: center;
  461 + padding: 20rpx 48rpx;
  462 + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
  463 + border-radius: 48rpx;
  464 + box-shadow: 0 8rpx 24rpx rgba(67, 233, 123, 0.3);
  465 + }
  466 +
  467 + .add-text {
  468 + font-size: 28rpx;
  469 + color: #fff;
  470 + font-weight: 600;
  471 + letter-spacing: 2rpx;
  472 + }
  473 +
  474 + .list-card {
  475 + background: #fff;
  476 + border-radius: 24rpx;
  477 + overflow: hidden;
  478 + box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.1);
  479 + }
  480 +
  481 + .list-header {
  482 + display: flex;
  483 + justify-content: space-between;
  484 + align-items: center;
  485 + padding: 32rpx;
  486 + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%);
  487 + }
  488 +
  489 + .header-text {
  490 + font-size: 32rpx;
  491 + color: #fff;
  492 + font-weight: 600;
  493 + letter-spacing: 2rpx;
  494 + }
  495 +
  496 + .total-count {
  497 + font-size: 24rpx;
  498 + color: #fff;
  499 + opacity: 0.9;
  500 + }
  501 +
  502 + .list-content {
  503 + max-height: calc(100vh - 500rpx);
  504 + }
  505 +
  506 + .list-item {
  507 + padding: 32rpx;
  508 + border-bottom: 2rpx solid #f0f0f0;
  509 + transition: all 0.2s ease;
  510 + }
  511 +
  512 + .list-item:last-child {
  513 + border-bottom: none;
  514 + }
  515 +
  516 + .list-item:active {
  517 + background: #f9fff9;
  518 + }
  519 +
  520 + .item-header {
  521 + display: flex;
  522 + justify-content: space-between;
  523 + align-items: center;
  524 + margin-bottom: 16rpx;
  525 + }
  526 +
  527 + .applicant-name {
  528 + font-size: 32rpx;
  529 + color: #2e7d32;
  530 + font-weight: 600;
  531 + }
  532 +
  533 + .application-time {
  534 + font-size: 24rpx;
  535 + color: #6a9c6a;
  536 + }
  537 +
  538 + .item-details {
  539 + display: flex;
  540 + flex-wrap: wrap;
  541 + gap: 24rpx;
  542 + margin-bottom: 16rpx;
  543 + }
  544 +
  545 + .detail-item {
  546 + display: flex;
  547 + align-items: center;
  548 + gap: 8rpx;
  549 + }
  550 +
  551 + .detail-label {
  552 + font-size: 24rpx;
  553 + color: #6a9c6a;
  554 + }
  555 +
  556 + .detail-value {
  557 + font-size: 28rpx;
  558 + color: #2e7d32;
  559 + font-weight: 500;
  560 + }
  561 +
  562 + .detail-value.amount {
  563 + color: #f57c00;
  564 + font-weight: 600;
  565 + font-size: 32rpx;
  566 + }
  567 +
  568 + .item-footer {
  569 + display: flex;
  570 + justify-content: space-between;
  571 + align-items: center;
  572 + margin-top: 16rpx;
  573 + }
  574 +
  575 + .status-badge {
  576 + display: inline-block;
  577 + padding: 8rpx 24rpx;
  578 + border-radius: 40rpx;
  579 + font-size: 24rpx;
  580 + font-weight: 500;
  581 + text-align: center;
  582 + }
  583 +
  584 + .status-badge.unapproved {
  585 + background: #fff3e0;
  586 + color: #ef6c00;
  587 + border: 2rpx solid #ffe0b2;
  588 + }
  589 +
  590 + .status-badge.pending {
  591 + background: #e3f2fd;
  592 + color: #1976d2;
  593 + border: 2rpx solid #bbdefb;
  594 + }
  595 +
  596 + .status-badge.approved {
  597 + background: #e8f5e9;
  598 + color: #2e7d32;
  599 + border: 2rpx solid #c8e6c9;
  600 + }
  601 +
  602 + .status-badge.rejected {
  603 + background: #ffebee;
  604 + color: #c62828;
  605 + border: 2rpx solid #ffcdd2;
  606 + }
  607 +
  608 + .action-buttons {
  609 + display: flex;
  610 + align-items: center;
  611 + gap: 16rpx;
  612 + }
  613 +
  614 + .action-btn {
  615 + display: flex;
  616 + align-items: center;
  617 + padding: 12rpx 24rpx;
  618 + border-radius: 24rpx;
  619 + font-size: 24rpx;
  620 + font-weight: 500;
  621 + transition: all 0.2s ease;
  622 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  623 + }
  624 +
  625 + .action-btn:active {
  626 + transform: scale(0.95);
  627 + box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.15);
  628 + }
  629 +
  630 + .view-btn {
  631 + background: linear-gradient(135deg, #42a5f5 0%, #1e88e5 100%);
  632 + color: #fff;
  633 + }
  634 +
  635 + .view-btn:active {
  636 + background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
  637 + }
  638 +
  639 + .edit-btn {
  640 + background: linear-gradient(135deg, #66bb6a 0%, #43a047 100%);
  641 + color: #fff;
  642 + }
  643 +
  644 + .edit-btn:active {
  645 + background: linear-gradient(135deg, #43a047 0%, #388e3c 100%);
  646 + }
  647 +
  648 + .btn-text {
  649 + letter-spacing: 1rpx;
  650 + }
  651 +
  652 + .loading {
  653 + text-align: center;
  654 + padding: 80rpx 40rpx;
  655 + color: #6a9c6a;
  656 + font-size: 28rpx;
  657 + }
  658 +
  659 + .empty-state {
  660 + text-align: center;
  661 + padding: 120rpx 40rpx;
  662 + color: #6a9c6a;
  663 + }
  664 +
  665 + .empty-icon {
  666 + font-size: 120rpx;
  667 + margin-bottom: 32rpx;
  668 + opacity: 0.5;
  669 + }
  670 +</style>
  671 +
... ...
绿纤uni-app/pages/reimbursement-list/reimbursement-list.vue
... ... @@ -58,14 +58,14 @@
58 58 </view>
59 59 </view>
60 60 <view class="item-footer">
61   - <view class="status-badge" :class="item.approveStatus=='已审批'?'approved':item.approveStatus=='待审批'?'pending':item.approveStatus=='未通过'?'rejected':'unapproved'">
62   - {{ item.approveStatus || '未审批' }}
  61 + <view class="status-badge" :class="item.approveStatus=='待审批'?'pending':item.approveStatus=='审批中'?'pending':item.approveStatus=='已通过'?'approved':item.approveStatus=='未通过'?'rejected':item.approveStatus=='已退回'?'rejected':'unapproved'">
  62 + {{ item.approveStatus || '待审批' }}
63 63 </view>
64 64 <view class="action-buttons">
65 65 <view class="action-btn view-btn" @click.stop="handleView(item)">
66 66 <text class="btn-text">查看</text>
67 67 </view>
68   - <view v-if="item.approveStatus === '未审批'" class="action-btn edit-btn" @click.stop="handleEdit(item)">
  68 + <view v-if="item.approveStatus === '已退回' || item.approveStatus === '未通过'" class="action-btn edit-btn" @click.stop="handleEdit(item)">
69 69 <text class="btn-text">编辑</text>
70 70 </view>
71 71 </view>
... ... @@ -138,16 +138,18 @@
138 138 approveStatusIndex: 0,
139 139 approveStatusOptions: [
140 140 { label: '全部', value: '' },
141   - { label: '未审批', value: '未审批' },
142 141 { label: '待审批', value: '待审批' },
143   - { label: '已审批', value: '已审批' },
144   - { label: '未通过', value: '未通过' }
  142 + { label: '审批中', value: '审批中' },
  143 + { label: '已通过', value: '已通过' },
  144 + { label: '未通过', value: '未通过' },
  145 + { label: '已退回', value: '已退回' }
145 146 ]
146 147 }
147 148 },
148 149  
149   - onLoad() {
  150 + onShow() {
150 151 // this.loadStoreOptions()
  152 + this.currentPage = 1
151 153 this.loadReimbursementList()
152 154 },
153 155  
... ... @@ -320,15 +322,15 @@
320 322 })
321 323 },
322 324  
323   - // 编辑记录(只有未审批状态才能编辑)
  325 + // 编辑记录(已退回或未通过状态才能编辑)
324 326 handleEdit(item) {
325   - if (item.approveStatus === '未审批') {
  327 + if (item.approveStatus === '已退回' || item.approveStatus === '未通过') {
326 328 uni.navigateTo({
327 329 url: `/pages/reimbursement-form/reimbursement-form?id=${item.id}`
328 330 })
329 331 } else {
330 332 uni.showToast({
331   - title: '已审批的记录不能编辑',
  333 + title: '该状态的记录不能编辑',
332 334 icon: 'none'
333 335 })
334 336 }
... ... @@ -336,8 +338,8 @@
336 338  
337 339 // 处理列表项点击(用于编辑)
338 340 handleItemClick(item) {
339   - // 只有未审批状态才能编辑
340   - if (item.approveStatus === '未审批') {
  341 + // 已退回或未通过状态才能编辑
  342 + if (item.approveStatus === '已退回' || item.approveStatus === '未通过') {
341 343 this.handleEdit(item)
342 344 }
343 345 },
... ... @@ -353,21 +355,6 @@
353 355 hour: '2-digit',
354 356 minute: '2-digit'
355 357 })
356   - },
357   -
358   - // 获取状态样式类
359   - getStatusClass(status) {
360   - switch (status) {
361   - case '已审批':
362   - return 'approved'
363   - case '待审批':
364   - return 'pending'
365   - case '未通过':
366   - return 'rejected'
367   - case '未审批':
368   - default:
369   - return 'unapproved'
370   - }
371 358 }
372 359 }
373 360 }
... ...