DemoPage.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <script setup lang="ts">
  2. import { onMounted, reactive, ref, computed } from 'vue'
  3. import { ElMessage } from 'element-plus'
  4. import {
  5. Plus,
  6. EditPen,
  7. Upload,
  8. View,
  9. Close
  10. } from '@element-plus/icons-vue'
  11. import { reimburseApi, expenseItemApi } from '../../api'
  12. import { useExpenseItemTree, formatExpenseName } from '../../hooks/useExpenseItemTree'
  13. import { statusLabel, statusTone, formatMoney, moneyTableFormatter } from '../../utils/format'
  14. import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
  15. import BaseButton from '../../components/base/BaseButton.vue'
  16. import BaseDialog from '../../components/base/BaseDialog.vue'
  17. import BaseInput from '../../components/base/BaseInput.vue'
  18. import BaseSelect from '../../components/base/BaseSelect.vue'
  19. import BaseTag from '../../components/base/BaseTag.vue'
  20. import BaseOpButton from '../../components/base/BaseOpButton.vue'
  21. import BaseClampText from '../../components/base/BaseClampText.vue'
  22. import { useServerPaged } from '../../hooks/useServerPaged'
  23. type DialogKey = 'addReimburse' | 'viewReimburse' | 'viewFlow'
  24. const showMoreFilters = ref(false)
  25. const loadingMap = reactive<Record<string, boolean>>({})
  26. const dialogs = reactive<Record<DialogKey, boolean>>({
  27. addReimburse: false,
  28. viewReimburse: false,
  29. viewFlow: false
  30. })
  31. const myReimburseStats = ref({
  32. pendingCount: 0,
  33. monthApplyAmount: 0,
  34. approvedAmount: 0,
  35. approvedCount: 0,
  36. })
  37. async function loadMyReimburseStats() {
  38. try {
  39. const res: any = await reimburseApi.myStats()
  40. const d = res.data || {}
  41. myReimburseStats.value = {
  42. pendingCount: Number(d.pendingCount ?? 0),
  43. monthApplyAmount: Number(d.monthApplyAmount ?? 0),
  44. approvedAmount: Number(d.approvedAmount ?? 0),
  45. approvedCount: Number(d.approvedCount ?? 0),
  46. }
  47. } catch {
  48. /* 拦截器已提示 */
  49. }
  50. }
  51. const reimburseStats = computed(() => {
  52. const s = myReimburseStats.value
  53. return [
  54. { name: '待审批申请', value: String(s.pendingCount) },
  55. { name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
  56. { name: '已审批金额(元)', value: formatMoney(s.approvedAmount) },
  57. { name: '已通过申请', value: String(s.approvedCount) },
  58. ]
  59. })
  60. const reimbursePager = useServerPaged<any>(async (params) => {
  61. const res: any = await reimburseApi.my(params)
  62. return res.data
  63. }, { initialPageSize: 10 })
  64. const selectedRows = ref<any[]>([])
  65. function onSelectionChange(rows: any[]) {
  66. selectedRows.value = rows
  67. }
  68. function handleToolbarEdit() {
  69. if (selectedRows.value.length === 0) {
  70. ElMessage.warning('请先勾选要编辑的报销单')
  71. return
  72. }
  73. ElMessage.info('编辑功能开发中')
  74. }
  75. function handleToolbarSubmit() {
  76. if (selectedRows.value.length === 0) {
  77. ElMessage.warning('请先勾选要提交的报销单')
  78. return
  79. }
  80. loadingMap.submit = true
  81. setTimeout(() => {
  82. loadingMap.submit = false
  83. ElMessage.success('提交成功')
  84. }, 800)
  85. }
  86. function handleSearch() {
  87. reimbursePager.refresh()
  88. }
  89. function handleReset() {
  90. reimbursePager.search()
  91. }
  92. function toggleMoreFilters() {
  93. showMoreFilters.value = !showMoreFilters.value
  94. }
  95. function reimburseSummaryMethod(params: { data: Array<{ amount?: number }> }) {
  96. const totalAmount = params.data.reduce((sum, row) => sum + Number(row.amount || 0), 0)
  97. return [
  98. '',
  99. '',
  100. `本页合计(${params.data.length}单)`,
  101. '',
  102. '',
  103. '',
  104. formatMoney(totalAmount),
  105. '',
  106. '',
  107. ''
  108. ]
  109. }
  110. function showVisualMessages() {
  111. ElMessage.success('成功消息:主题色与状态色已对齐')
  112. setTimeout(() => ElMessage.warning('警告消息:请核对 warning 色值表现'), 150)
  113. setTimeout(() => ElMessage.error('错误消息:请核对 danger 色值表现'), 300)
  114. setTimeout(() => ElMessage.info('信息消息:请核对 info 色值表现'), 450)
  115. }
  116. const visualCheckSwitch = ref(true)
  117. const { expenseTree, flatList } = useExpenseItemTree()
  118. const addForm = reactive({
  119. title: '',
  120. amount: '',
  121. expenseId: '' as string | number,
  122. remark: ''
  123. })
  124. function handleAddReimburse() {
  125. loadingMap.addSubmit = true
  126. setTimeout(() => {
  127. loadingMap.addSubmit = false
  128. ElMessage.success('新增成功(demo)')
  129. dialogs.addReimburse = false
  130. Object.assign(addForm, { title: '', amount: '', expenseId: '', remark: '' })
  131. }, 800)
  132. }
  133. onMounted(() => {
  134. reimbursePager.refresh()
  135. loadMyReimburseStats()
  136. })
  137. </script>
  138. <template>
  139. <div>
  140. <div class="page-head">
  141. <h1>业务界面demo</h1>
  142. <p class="desc">本页面为所有业务界面的设计范本,包含完整的组件使用示例。后续页面参照此页面开发。</p>
  143. </div>
  144. <div class="stats">
  145. <div v-for="item in reimburseStats" :key="item.name" class="stat">
  146. <p class="k">{{ item.name }}</p>
  147. <p class="v">{{ item.value }}</p>
  148. </div>
  149. </div>
  150. <section class="visual-check">
  151. <div class="visual-check-head">
  152. <h3>临时视觉抽查区(验收后移除)</h3>
  153. <BaseButton :icon="View" @click="showVisualMessages">触发 Message</BaseButton>
  154. </div>
  155. <div class="visual-check-group">
  156. <span class="visual-check-label">Tag</span>
  157. <el-tag type="success">成功</el-tag>
  158. <el-tag type="warning">警告</el-tag>
  159. <el-tag type="danger">错误</el-tag>
  160. <el-tag type="info">信息</el-tag>
  161. </div>
  162. <div class="visual-check-group visual-check-alerts">
  163. <span class="visual-check-label">Alert</span>
  164. <el-alert title="Success 状态" type="success" :closable="false" />
  165. <el-alert title="Warning 状态" type="warning" :closable="false" />
  166. <el-alert title="Error 状态" type="error" :closable="false" />
  167. <el-alert title="Info 状态" type="info" :closable="false" />
  168. </div>
  169. <div class="visual-check-group">
  170. <span class="visual-check-label">Switch</span>
  171. <el-switch v-model="visualCheckSwitch" />
  172. <span class="visual-check-switch-text">{{ visualCheckSwitch ? '开启' : '关闭' }}</span>
  173. </div>
  174. </section>
  175. <ListPageScaffold
  176. panel-title="报销单列表"
  177. :show-more="showMoreFilters"
  178. :has-expandable-filters="true"
  179. :selected-count="2"
  180. :rows="reimbursePager.rows.value"
  181. :range-text="reimbursePager.rangeText.value"
  182. :total="reimbursePager.total.value"
  183. :current-page="reimbursePager.currentPage.value"
  184. :page-size="reimbursePager.pageSize.value"
  185. :show-summary="true"
  186. :summary-method="reimburseSummaryMethod"
  187. :table-max-height="'min(520px, calc(100vh - 300px))'"
  188. @toggle-more="toggleMoreFilters"
  189. @reset="handleReset"
  190. @search="handleSearch"
  191. @update:current-page="reimbursePager.currentPage.value = $event"
  192. @update:page-size="reimbursePager.pageSize.value = $event"
  193. @size-change="reimbursePager.onSizeChange"
  194. >
  195. <template #filters>
  196. <div class="filter-grid">
  197. <div class="filter-field">
  198. <label>单号</label>
  199. <BaseInput model-value="BX20260424001" />
  200. </div>
  201. <div class="filter-field">
  202. <label>费用项目</label>
  203. <el-tree-select
  204. model-value="差旅费/住宿费"
  205. :data="expenseTree"
  206. :props="{ label: 'itemName', value: 'itemName', children: 'children' }"
  207. placeholder="请选择费用项目"
  208. check-strictly
  209. :render-after-expand="false"
  210. class="fill-100"
  211. disabled
  212. />
  213. </div>
  214. <div class="filter-field">
  215. <label>状态</label>
  216. <BaseSelect model-value="审批中"><el-option label="审批中" value="审批中" /></BaseSelect>
  217. </div>
  218. <template v-if="showMoreFilters">
  219. <div class="filter-field">
  220. <label>申请人</label>
  221. <BaseInput model-value="李老师" />
  222. </div>
  223. <div class="filter-field">
  224. <label>单据日期</label>
  225. <BaseInput model-value="2026-04-24" />
  226. </div>
  227. <div class="filter-field">
  228. <label>审批节点</label>
  229. <BaseSelect model-value="财务复核"><el-option label="财务复核" value="财务复核" /></BaseSelect>
  230. </div>
  231. <div class="filter-field">
  232. <label>是否冲抵</label>
  233. <BaseSelect model-value="否">
  234. <el-option label="否" value="否" />
  235. <el-option label="是" value="是" />
  236. </BaseSelect>
  237. </div>
  238. </template>
  239. </div>
  240. </template>
  241. <template #toolbar>
  242. <BaseButton type="primary" plain :icon="Plus" @click="dialogs.addReimburse = true">新增报销单</BaseButton>
  243. <BaseButton :icon="EditPen" @click="handleToolbarEdit">编辑</BaseButton>
  244. <BaseButton :loading="loadingMap.submit" :icon="Upload" @click="handleToolbarSubmit">提交审批</BaseButton>
  245. <BaseButton :icon="View" @click="dialogs.viewFlow = true">查看审批流</BaseButton>
  246. </template>
  247. <template #table-columns>
  248. <el-table-column type="selection" width="48" />
  249. <el-table-column type="index" label="序号" width="90" />
  250. <el-table-column prop="code" label="报销单号" min-width="140" />
  251. <el-table-column prop="user" label="申请人" min-width="100" />
  252. <el-table-column prop="project" label="费用项目" min-width="160" />
  253. <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
  254. <el-table-column prop="invoiceCount" label="发票" min-width="80" />
  255. <el-table-column prop="attachCount" label="附件" min-width="80" />
  256. <el-table-column prop="status" label="审批状态" min-width="110">
  257. <template #default="{ row }">
  258. <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
  259. </template>
  260. </el-table-column>
  261. <el-table-column label="操作" width="180" fixed="right" class-name="col-actions">
  262. <template #default>
  263. <BaseOpButton
  264. primary
  265. :icon="View"
  266. @click="dialogs.viewReimburse = true"
  267. >
  268. 查看
  269. </BaseOpButton>
  270. <BaseOpButton :icon="EditPen" @click="handleToolbarEdit">
  271. 编辑
  272. </BaseOpButton>
  273. </template>
  274. </el-table-column>
  275. </template>
  276. </ListPageScaffold>
  277. <BaseDialog v-model="dialogs.addReimburse" title="新增报销单" width="720px">
  278. <p class="dlg-note">提交后进入顺序审批流程,可在"查看审批流"中确认节点与审批人。</p>
  279. <div class="form-section">
  280. <h4>基础信息</h4>
  281. <div class="form-grid-2">
  282. <div class="field">
  283. <label>报销标题</label>
  284. <BaseInput v-model="addForm.title" placeholder="请输入报销标题" />
  285. </div>
  286. <div class="field">
  287. <label>报销金额(元)</label>
  288. <BaseInput v-model="addForm.amount" placeholder="请输入金额" />
  289. </div>
  290. <div class="field">
  291. <label>费用项目</label>
  292. <el-tree-select
  293. v-model="addForm.expenseId"
  294. :data="expenseTree"
  295. :props="{ label: 'itemName', value: 'id', children: 'children' }"
  296. placeholder="请选择费用项目"
  297. check-strictly
  298. :render-after-expand="false"
  299. class="fill-100"
  300. />
  301. </div>
  302. </div>
  303. </div>
  304. <div class="form-section">
  305. <h4>备注说明</h4>
  306. <div class="field field-no-margin">
  307. <label class="sr-only">报销说明</label>
  308. <BaseInput type="textarea" :rows="4" v-model="addForm.remark" placeholder="请输入报销说明" />
  309. </div>
  310. </div>
  311. <template #footer>
  312. <BaseButton :icon="Close" @click="dialogs.addReimburse = false">取消</BaseButton>
  313. <BaseButton type="primary" :loading="loadingMap.addSubmit" :icon="Upload" @click="handleAddReimburse">保存</BaseButton>
  314. </template>
  315. </BaseDialog>
  316. <BaseDialog v-model="dialogs.viewReimburse" title="报销单详情" width="920px" class="dialog-view-xl">
  317. <div class="dlg-view">
  318. <div class="dlg-view-hero">
  319. <div class="dlg-view-hero-top">
  320. <h2 class="dlg-view-title">BX20260424001</h2>
  321. <div class="dlg-view-tags">
  322. <BaseTag :tone="statusTone('审批中')">审批中</BaseTag>
  323. </div>
  324. </div>
  325. <p class="dlg-view-meta">金额(元):<b class="money-em">¥2,680.00</b> · 发票 3 张 · 附件 2 个</p>
  326. </div>
  327. <div class="dlg-view-section">
  328. <div class="dlg-kv dlg-kv--triple">
  329. <div class="k">申请人</div>
  330. <div class="v">李老师</div>
  331. <div class="k">费用项目</div>
  332. <div class="v"><BaseClampText text="差旅费 / 住宿费" /></div>
  333. <div class="k">单据日期</div>
  334. <div class="v">2026-04-24</div>
  335. <div class="k">是否冲抵</div>
  336. <div class="v">否</div>
  337. </div>
  338. </div>
  339. <div class="dlg-view-section">
  340. <div class="dlg-kv dlg-kv--triple">
  341. <div class="k">报销说明</div>
  342. <div class="v kv-full"><BaseClampText text="培训住宿和交通费用报销" /></div>
  343. <div class="k">审批节点</div>
  344. <div class="v">财务复核</div>
  345. </div>
  346. </div>
  347. </div>
  348. </BaseDialog>
  349. <BaseDialog v-model="dialogs.viewFlow" title="审批流程" width="920px" class="dialog-view-xl">
  350. <p>1. 发起人主管(已通过)</p>
  351. <p>2. 财务复核(处理中)</p>
  352. <p>3. 总经理审批(待处理)</p>
  353. </BaseDialog>
  354. </div>
  355. </template>