| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- <script setup lang="ts">
- import { onMounted, reactive, ref, computed } from 'vue'
- import { ElMessage } from 'element-plus'
- import {
- Plus,
- EditPen,
- Upload,
- View,
- Close
- } from '@element-plus/icons-vue'
- import { reimburseApi, expenseItemApi } from '../../api'
- import { useExpenseItemTree, formatExpenseName } from '../../hooks/useExpenseItemTree'
- import { statusLabel, statusTone, formatMoney, moneyTableFormatter } from '../../utils/format'
- import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
- import BaseButton from '../../components/base/BaseButton.vue'
- import BaseDialog from '../../components/base/BaseDialog.vue'
- import BaseInput from '../../components/base/BaseInput.vue'
- import BaseSelect from '../../components/base/BaseSelect.vue'
- import BaseTag from '../../components/base/BaseTag.vue'
- import BaseOpButton from '../../components/base/BaseOpButton.vue'
- import BaseClampText from '../../components/base/BaseClampText.vue'
- import { useServerPaged } from '../../hooks/useServerPaged'
- type DialogKey = 'addReimburse' | 'viewReimburse' | 'viewFlow'
- const showMoreFilters = ref(false)
- const loadingMap = reactive<Record<string, boolean>>({})
- const dialogs = reactive<Record<DialogKey, boolean>>({
- addReimburse: false,
- viewReimburse: false,
- viewFlow: false
- })
- const myReimburseStats = ref({
- pendingCount: 0,
- monthApplyAmount: 0,
- approvedAmount: 0,
- approvedCount: 0,
- })
- async function loadMyReimburseStats() {
- try {
- const res: any = await reimburseApi.myStats()
- const d = res.data || {}
- myReimburseStats.value = {
- pendingCount: Number(d.pendingCount ?? 0),
- monthApplyAmount: Number(d.monthApplyAmount ?? 0),
- approvedAmount: Number(d.approvedAmount ?? 0),
- approvedCount: Number(d.approvedCount ?? 0),
- }
- } catch {
- /* 拦截器已提示 */
- }
- }
- const reimburseStats = computed(() => {
- const s = myReimburseStats.value
- return [
- { name: '待审批申请', value: String(s.pendingCount) },
- { name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
- { name: '已审批金额(元)', value: formatMoney(s.approvedAmount) },
- { name: '已通过申请', value: String(s.approvedCount) },
- ]
- })
- const reimbursePager = useServerPaged<any>(async (params) => {
- const res: any = await reimburseApi.my(params)
- return res.data
- }, { initialPageSize: 10 })
- const selectedRows = ref<any[]>([])
- function onSelectionChange(rows: any[]) {
- selectedRows.value = rows
- }
- function handleToolbarEdit() {
- if (selectedRows.value.length === 0) {
- ElMessage.warning('请先勾选要编辑的报销单')
- return
- }
- ElMessage.info('编辑功能开发中')
- }
- function handleToolbarSubmit() {
- if (selectedRows.value.length === 0) {
- ElMessage.warning('请先勾选要提交的报销单')
- return
- }
- loadingMap.submit = true
- setTimeout(() => {
- loadingMap.submit = false
- ElMessage.success('提交成功')
- }, 800)
- }
- function handleSearch() {
- reimbursePager.refresh()
- }
- function handleReset() {
- reimbursePager.search()
- }
- function toggleMoreFilters() {
- showMoreFilters.value = !showMoreFilters.value
- }
- function reimburseSummaryMethod(params: { data: Array<{ amount?: number }> }) {
- const totalAmount = params.data.reduce((sum, row) => sum + Number(row.amount || 0), 0)
- return [
- '',
- '',
- `本页合计(${params.data.length}单)`,
- '',
- '',
- '',
- formatMoney(totalAmount),
- '',
- '',
- ''
- ]
- }
- function showVisualMessages() {
- ElMessage.success('成功消息:主题色与状态色已对齐')
- setTimeout(() => ElMessage.warning('警告消息:请核对 warning 色值表现'), 150)
- setTimeout(() => ElMessage.error('错误消息:请核对 danger 色值表现'), 300)
- setTimeout(() => ElMessage.info('信息消息:请核对 info 色值表现'), 450)
- }
- const visualCheckSwitch = ref(true)
- const { expenseTree, flatList } = useExpenseItemTree()
- const addForm = reactive({
- title: '',
- amount: '',
- expenseId: '' as string | number,
- remark: ''
- })
- function handleAddReimburse() {
- loadingMap.addSubmit = true
- setTimeout(() => {
- loadingMap.addSubmit = false
- ElMessage.success('新增成功(demo)')
- dialogs.addReimburse = false
- Object.assign(addForm, { title: '', amount: '', expenseId: '', remark: '' })
- }, 800)
- }
- onMounted(() => {
- reimbursePager.refresh()
- loadMyReimburseStats()
- })
- </script>
- <template>
- <div>
- <div class="page-head">
- <h1>业务界面demo</h1>
- <p class="desc">本页面为所有业务界面的设计范本,包含完整的组件使用示例。后续页面参照此页面开发。</p>
- </div>
- <div class="stats">
- <div v-for="item in reimburseStats" :key="item.name" class="stat">
- <p class="k">{{ item.name }}</p>
- <p class="v">{{ item.value }}</p>
- </div>
- </div>
- <section class="visual-check">
- <div class="visual-check-head">
- <h3>临时视觉抽查区(验收后移除)</h3>
- <BaseButton :icon="View" @click="showVisualMessages">触发 Message</BaseButton>
- </div>
- <div class="visual-check-group">
- <span class="visual-check-label">Tag</span>
- <el-tag type="success">成功</el-tag>
- <el-tag type="warning">警告</el-tag>
- <el-tag type="danger">错误</el-tag>
- <el-tag type="info">信息</el-tag>
- </div>
- <div class="visual-check-group visual-check-alerts">
- <span class="visual-check-label">Alert</span>
- <el-alert title="Success 状态" type="success" :closable="false" />
- <el-alert title="Warning 状态" type="warning" :closable="false" />
- <el-alert title="Error 状态" type="error" :closable="false" />
- <el-alert title="Info 状态" type="info" :closable="false" />
- </div>
- <div class="visual-check-group">
- <span class="visual-check-label">Switch</span>
- <el-switch v-model="visualCheckSwitch" />
- <span class="visual-check-switch-text">{{ visualCheckSwitch ? '开启' : '关闭' }}</span>
- </div>
- </section>
- <ListPageScaffold
- panel-title="报销单列表"
- :show-more="showMoreFilters"
- :has-expandable-filters="true"
- :selected-count="2"
- :rows="reimbursePager.rows.value"
- :range-text="reimbursePager.rangeText.value"
- :total="reimbursePager.total.value"
- :current-page="reimbursePager.currentPage.value"
- :page-size="reimbursePager.pageSize.value"
- :show-summary="true"
- :summary-method="reimburseSummaryMethod"
- :table-max-height="'min(520px, calc(100vh - 300px))'"
- @toggle-more="toggleMoreFilters"
- @reset="handleReset"
- @search="handleSearch"
- @update:current-page="reimbursePager.currentPage.value = $event"
- @update:page-size="reimbursePager.pageSize.value = $event"
- @size-change="reimbursePager.onSizeChange"
- >
- <template #filters>
- <div class="filter-grid">
- <div class="filter-field">
- <label>单号</label>
- <BaseInput model-value="BX20260424001" />
- </div>
- <div class="filter-field">
- <label>费用项目</label>
- <el-tree-select
- model-value="差旅费/住宿费"
- :data="expenseTree"
- :props="{ label: 'itemName', value: 'itemName', children: 'children' }"
- placeholder="请选择费用项目"
- check-strictly
- :render-after-expand="false"
- class="fill-100"
- disabled
- />
- </div>
- <div class="filter-field">
- <label>状态</label>
- <BaseSelect model-value="审批中"><el-option label="审批中" value="审批中" /></BaseSelect>
- </div>
- <template v-if="showMoreFilters">
- <div class="filter-field">
- <label>申请人</label>
- <BaseInput model-value="李老师" />
- </div>
- <div class="filter-field">
- <label>单据日期</label>
- <BaseInput model-value="2026-04-24" />
- </div>
- <div class="filter-field">
- <label>审批节点</label>
- <BaseSelect model-value="财务复核"><el-option label="财务复核" value="财务复核" /></BaseSelect>
- </div>
- <div class="filter-field">
- <label>是否冲抵</label>
- <BaseSelect model-value="否">
- <el-option label="否" value="否" />
- <el-option label="是" value="是" />
- </BaseSelect>
- </div>
- </template>
- </div>
- </template>
- <template #toolbar>
- <BaseButton type="primary" plain :icon="Plus" @click="dialogs.addReimburse = true">新增报销单</BaseButton>
- <BaseButton :icon="EditPen" @click="handleToolbarEdit">编辑</BaseButton>
- <BaseButton :loading="loadingMap.submit" :icon="Upload" @click="handleToolbarSubmit">提交审批</BaseButton>
- <BaseButton :icon="View" @click="dialogs.viewFlow = true">查看审批流</BaseButton>
- </template>
- <template #table-columns>
- <el-table-column type="selection" width="48" />
- <el-table-column type="index" label="序号" width="90" />
- <el-table-column prop="code" label="报销单号" min-width="140" />
- <el-table-column prop="user" label="申请人" min-width="100" />
- <el-table-column prop="project" label="费用项目" min-width="160" />
- <el-table-column prop="amount" label="报销金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
- <el-table-column prop="invoiceCount" label="发票" min-width="80" />
- <el-table-column prop="attachCount" label="附件" min-width="80" />
- <el-table-column prop="status" label="审批状态" min-width="110">
- <template #default="{ row }">
- <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="180" fixed="right" class-name="col-actions">
- <template #default>
- <BaseOpButton
- primary
- :icon="View"
- @click="dialogs.viewReimburse = true"
- >
- 查看
- </BaseOpButton>
- <BaseOpButton :icon="EditPen" @click="handleToolbarEdit">
- 编辑
- </BaseOpButton>
- </template>
- </el-table-column>
- </template>
- </ListPageScaffold>
- <BaseDialog v-model="dialogs.addReimburse" title="新增报销单" width="720px">
- <p class="dlg-note">提交后进入顺序审批流程,可在"查看审批流"中确认节点与审批人。</p>
- <div class="form-section">
- <h4>基础信息</h4>
- <div class="form-grid-2">
- <div class="field">
- <label>报销标题</label>
- <BaseInput v-model="addForm.title" placeholder="请输入报销标题" />
- </div>
- <div class="field">
- <label>报销金额(元)</label>
- <BaseInput v-model="addForm.amount" placeholder="请输入金额" />
- </div>
- <div class="field">
- <label>费用项目</label>
- <el-tree-select
- v-model="addForm.expenseId"
- :data="expenseTree"
- :props="{ label: 'itemName', value: 'id', children: 'children' }"
- placeholder="请选择费用项目"
- check-strictly
- :render-after-expand="false"
- class="fill-100"
- />
- </div>
- </div>
- </div>
- <div class="form-section">
- <h4>备注说明</h4>
- <div class="field field-no-margin">
- <label class="sr-only">报销说明</label>
- <BaseInput type="textarea" :rows="4" v-model="addForm.remark" placeholder="请输入报销说明" />
- </div>
- </div>
- <template #footer>
- <BaseButton :icon="Close" @click="dialogs.addReimburse = false">取消</BaseButton>
- <BaseButton type="primary" :loading="loadingMap.addSubmit" :icon="Upload" @click="handleAddReimburse">保存</BaseButton>
- </template>
- </BaseDialog>
- <BaseDialog v-model="dialogs.viewReimburse" title="报销单详情" width="920px" class="dialog-view-xl">
- <div class="dlg-view">
- <div class="dlg-view-hero">
- <div class="dlg-view-hero-top">
- <h2 class="dlg-view-title">BX20260424001</h2>
- <div class="dlg-view-tags">
- <BaseTag :tone="statusTone('审批中')">审批中</BaseTag>
- </div>
- </div>
- <p class="dlg-view-meta">金额(元):<b class="money-em">¥2,680.00</b> · 发票 3 张 · 附件 2 个</p>
- </div>
- <div class="dlg-view-section">
- <div class="dlg-kv dlg-kv--triple">
- <div class="k">申请人</div>
- <div class="v">李老师</div>
- <div class="k">费用项目</div>
- <div class="v"><BaseClampText text="差旅费 / 住宿费" /></div>
- <div class="k">单据日期</div>
- <div class="v">2026-04-24</div>
- <div class="k">是否冲抵</div>
- <div class="v">否</div>
- </div>
- </div>
- <div class="dlg-view-section">
- <div class="dlg-kv dlg-kv--triple">
- <div class="k">报销说明</div>
- <div class="v kv-full"><BaseClampText text="培训住宿和交通费用报销" /></div>
- <div class="k">审批节点</div>
- <div class="v">财务复核</div>
- </div>
- </div>
- </div>
- </BaseDialog>
- <BaseDialog v-model="dialogs.viewFlow" title="审批流程" width="920px" class="dialog-view-xl">
- <p>1. 发起人主管(已通过)</p>
- <p>2. 财务复核(处理中)</p>
- <p>3. 总经理审批(待处理)</p>
- </BaseDialog>
- </div>
- </template>
|