|
@@ -0,0 +1,951 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
|
|
|
|
|
+ <DataTable
|
|
|
|
|
+ :value="tableData.content"
|
|
|
|
|
+ :paginator="true"
|
|
|
|
|
+ paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
|
|
|
|
|
+ currentPageReportTemplate="{totalRecords} 条记录 "
|
|
|
|
|
+ :rows="tableData.metadata.size"
|
|
|
|
|
+ :rowsPerPageOptions="[10, 20, 50, 100]"
|
|
|
|
|
+ :totalRecords="tableData.metadata.total"
|
|
|
|
|
+ @page="handlePageChange"
|
|
|
|
|
+ lazy
|
|
|
|
|
+ scrollable
|
|
|
|
|
+ class="finance-table"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
+ <InputText v-model="searchForm.id" placeholder="ID" size="small" class="w-32" @keyup.enter="handleSearch" />
|
|
|
|
|
+ <InputText
|
|
|
|
|
+ v-model="searchForm.paymentName"
|
|
|
|
|
+ placeholder="收款名称"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="w-32"
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <InputText
|
|
|
|
|
+ v-model="searchForm.paymentAccount"
|
|
|
|
|
+ placeholder="收款账户"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="w-32"
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <InputText
|
|
|
|
|
+ v-model="searchForm.bankName"
|
|
|
|
|
+ placeholder="开户行"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="w-32"
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Dropdown
|
|
|
|
|
+ v-model="searchForm.status"
|
|
|
|
|
+ :options="statusOptions"
|
|
|
|
|
+ optionLabel="label"
|
|
|
|
|
+ optionValue="value"
|
|
|
|
|
+ placeholder="状态"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="w-32"
|
|
|
|
|
+ :showClear="true"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DatePicker
|
|
|
|
|
+ v-model="searchForm.startDate"
|
|
|
|
|
+ placeholder="开始日期"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="w-40"
|
|
|
|
|
+ dateFormat="yy-mm-dd"
|
|
|
|
|
+ showIcon
|
|
|
|
|
+ />
|
|
|
|
|
+ <DatePicker
|
|
|
|
|
+ v-model="searchForm.endDate"
|
|
|
|
|
+ placeholder="结束日期"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ class="w-40"
|
|
|
|
|
+ dateFormat="yy-mm-dd"
|
|
|
|
|
+ showIcon
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
|
|
|
|
|
+ <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
|
|
|
|
|
+ <div class="flex-1"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="id" header="ID" style="min-width: 80px" frozen>
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="font-mono text-sm copyable-text"
|
|
|
|
|
+ :title="slotProps.data.id"
|
|
|
|
|
+ @click="copyToClipboard(slotProps.data.id)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ slotProps.data.id }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="reminderAmount" header="提现金额" style="min-width: 120px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span class="amount-text font-semibold"> {{ formatAmount(slotProps.data.reminderAmount) }} </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column
|
|
|
|
|
+ field="paymentQrCode"
|
|
|
|
|
+ header="收款码"
|
|
|
|
|
+ style="min-width: 150px"
|
|
|
|
|
+ :pt="{
|
|
|
|
|
+ columnHeaderContent: {
|
|
|
|
|
+ class: 'justify-center'
|
|
|
|
|
+ }
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <div v-if="slotProps.data.paymentQrCode" class="qr-code-container">
|
|
|
|
|
+ <img
|
|
|
|
|
+ :src="slotProps.data.paymentQrCode"
|
|
|
|
|
+ :alt="'收款码'"
|
|
|
|
|
+ class="qr-code-image"
|
|
|
|
|
+ @click="previewQrCode(slotProps.data.paymentQrCode)"
|
|
|
|
|
+ title="点击预览收款码"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span v-else class="text-gray-400 text-sm">-</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="paymentName" header="收款名称" style="min-width: 120px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="font-medium payment-name-text copyable-text"
|
|
|
|
|
+ :title="slotProps.data.paymentName"
|
|
|
|
|
+ @click="copyToClipboard(slotProps.data.paymentName)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ slotProps.data.paymentName }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="paymentAccount" header="收款账户" style="min-width: 150px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="font-mono text-sm payment-account-text copyable-text"
|
|
|
|
|
+ :title="slotProps.data.paymentAccount"
|
|
|
|
|
+ @click="copyToClipboard(slotProps.data.paymentAccount)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ slotProps.data.paymentAccount }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="bankName" header="开户行" style="min-width: 120px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span class="bank-name-text">
|
|
|
|
|
+ {{ slotProps.data.bankName }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="status" header="状态" style="min-width: 100px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span class="status-text" :class="getStatusClass(slotProps.data.status)">
|
|
|
|
|
+ {{ getStatusText(slotProps.data.status) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="rejectReason" header="驳回原因" style="min-width: 150px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <div class="text-sm reject-reason-text" :title="slotProps.data.rejectReason || '-'">
|
|
|
|
|
+ {{ slotProps.data.rejectReason || '-' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="remark" header="备注" style="min-width: 150px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <div class="text-sm remark-text remark-content" :title="slotProps.data.remark || '-'">
|
|
|
|
|
+ {{ slotProps.data.remark || '-' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column field="createdAt" header="创建时间" style="min-width: 150px">
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <span class="text-sm">
|
|
|
|
|
+ {{ formatDateTime(slotProps.data.createdAt) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+
|
|
|
|
|
+ <Column
|
|
|
|
|
+ header="操作"
|
|
|
|
|
+ style="min-width: 200px; width: 200px"
|
|
|
|
|
+ align-frozen="right"
|
|
|
|
|
+ frozen
|
|
|
|
|
+ :pt="{
|
|
|
|
|
+ columnHeaderContent: {
|
|
|
|
|
+ class: 'justify-center'
|
|
|
|
|
+ }
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #body="slotProps">
|
|
|
|
|
+ <div class="flex justify-center gap-1">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ v-if="slotProps.data.status === 'processing'"
|
|
|
|
|
+ icon="pi pi-check"
|
|
|
|
|
+ severity="success"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ text
|
|
|
|
|
+ rounded
|
|
|
|
|
+ @click="confirmApprove(slotProps.data)"
|
|
|
|
|
+ title="通过"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ v-if="slotProps.data.status === 'processing'"
|
|
|
|
|
+ icon="pi pi-times"
|
|
|
|
|
+ severity="danger"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ text
|
|
|
|
|
+ rounded
|
|
|
|
|
+ @click="openRejectDialog(slotProps.data)"
|
|
|
|
|
+ title="驳回"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-pencil"
|
|
|
|
|
+ severity="info"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ text
|
|
|
|
|
+ rounded
|
|
|
|
|
+ aria-label="编辑"
|
|
|
|
|
+ @click="openEditDialog(slotProps.data)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-trash"
|
|
|
|
|
+ severity="danger"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ text
|
|
|
|
|
+ rounded
|
|
|
|
|
+ aria-label="删除"
|
|
|
|
|
+ @click="confirmDelete(slotProps.data)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Column>
|
|
|
|
|
+ </DataTable>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 编辑弹窗 -->
|
|
|
|
|
+ <Dialog
|
|
|
|
|
+ v-model:visible="editDialog"
|
|
|
|
|
+ :modal="true"
|
|
|
|
|
+ header="编辑财务记录"
|
|
|
|
|
+ :style="{ width: '600px' }"
|
|
|
|
|
+ position="center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="p-fluid">
|
|
|
|
|
+ <div class="field">
|
|
|
|
|
+ <label for="edit-paymentName" class="font-medium text-sm mb-2 block">收款名称</label>
|
|
|
|
|
+ <InputText id="edit-paymentName" v-model="editForm.paymentName" class="w-full" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="field mt-4">
|
|
|
|
|
+ <label for="edit-reminderAmount" class="font-medium text-sm mb-2 block">提现金额</label>
|
|
|
|
|
+ <InputNumber
|
|
|
|
|
+ id="edit-reminderAmount"
|
|
|
|
|
+ v-model="editForm.reminderAmount"
|
|
|
|
|
+ mode="currency"
|
|
|
|
|
+ currency="CNY"
|
|
|
|
|
+ locale="zh-CN"
|
|
|
|
|
+ class="w-full"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="field mt-4">
|
|
|
|
|
+ <label class="font-medium text-sm mb-2 block">收款码</label>
|
|
|
|
|
+ <div class="card flex flex-col items-center gap-6">
|
|
|
|
|
+ <FileUpload
|
|
|
|
|
+ v-if="!qrCodePreview"
|
|
|
|
|
+ mode="basic"
|
|
|
|
|
+ @select="onQrCodeFileSelect"
|
|
|
|
|
+ customUpload
|
|
|
|
|
+ auto
|
|
|
|
|
+ severity="secondary"
|
|
|
|
|
+ class="p-button-outlined"
|
|
|
|
|
+ accept="image/*"
|
|
|
|
|
+ :maxFileSize="50000000"
|
|
|
|
|
+ chooseLabel="选择收款码"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div v-if="qrCodePreview" class="flex flex-col items-center gap-2">
|
|
|
|
|
+ <img
|
|
|
|
|
+ :src="qrCodePreview"
|
|
|
|
|
+ alt="收款码"
|
|
|
|
|
+ class="shadow-md rounded-xl w-full sm:w-64"
|
|
|
|
|
+ style="filter: grayscale(100%)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-times"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ rounded
|
|
|
|
|
+ severity="danger"
|
|
|
|
|
+ @click="removeQrCode"
|
|
|
|
|
+ title="移除收款码"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="field mt-4">
|
|
|
|
|
+ <label for="edit-paymentAccount" class="font-medium text-sm mb-2 block">收款账户</label>
|
|
|
|
|
+ <InputText id="edit-paymentAccount" v-model="editForm.paymentAccount" class="w-full" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="field mt-4">
|
|
|
|
|
+ <label for="edit-bankName" class="font-medium text-sm mb-2 block">开户行</label>
|
|
|
|
|
+ <InputText id="edit-bankName" v-model="editForm.bankName" class="w-full" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="field mt-4">
|
|
|
|
|
+ <label for="edit-remark" class="font-medium text-sm mb-2 block">备注</label>
|
|
|
|
|
+ <Textarea
|
|
|
|
|
+ id="edit-remark"
|
|
|
|
|
+ v-model="editForm.remark"
|
|
|
|
|
+ rows="3"
|
|
|
|
|
+ class="w-full"
|
|
|
|
|
+ placeholder="请输入备注信息..."
|
|
|
|
|
+ autoResize
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
|
|
+ <Button label="取消" severity="secondary" @click="editDialog = false" />
|
|
|
|
|
+ <Button label="保存" severity="success" @click="saveEdit" :loading="editLoading" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 拒绝确认弹窗 -->
|
|
|
|
|
+ <Dialog
|
|
|
|
|
+ v-model:visible="rejectDialog"
|
|
|
|
|
+ :modal="true"
|
|
|
|
|
+ header="提现申请驳回"
|
|
|
|
|
+ :style="{ width: '400px' }"
|
|
|
|
|
+ position="center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="p-fluid">
|
|
|
|
|
+ <div class="field">
|
|
|
|
|
+ <label for="reject-reason" class="font-medium text-sm mb-2 block">驳回原因(可选)</label>
|
|
|
|
|
+ <Textarea id="reject-reason" v-model="rejectReason" rows="3" class="w-full" autoResize />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
|
|
+ <Button label="取消" severity="secondary" @click="rejectDialog = false" />
|
|
|
|
|
+ <Button label="确认驳回" severity="danger" @click="confirmRejectSubmit" :loading="rejectLoading" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 二维码预览弹窗 -->
|
|
|
|
|
+ <Dialog
|
|
|
|
|
+ v-model:visible="qrCodeDialog"
|
|
|
|
|
+ :modal="true"
|
|
|
|
|
+ header="收款码预览"
|
|
|
|
|
+ :style="{ width: '400px' }"
|
|
|
|
|
+ position="center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="text-center">
|
|
|
|
|
+ <img v-if="qrCodeImage" :src="qrCodeImage" alt="收款码" class="max-w-full h-auto" />
|
|
|
|
|
+ <p v-else class="text-gray-500">暂无二维码图片</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
|
|
+import { useDateFormat } from '@vueuse/core'
|
|
|
|
|
+import Button from 'primevue/button'
|
|
|
|
|
+import Column from 'primevue/column'
|
|
|
|
|
+import DataTable from 'primevue/datatable'
|
|
|
|
|
+import DatePicker from 'primevue/calendar'
|
|
|
|
|
+import Dialog from 'primevue/dialog'
|
|
|
|
|
+import Dropdown from 'primevue/dropdown'
|
|
|
|
|
+import InputText from 'primevue/inputtext'
|
|
|
|
|
+import InputNumber from 'primevue/inputnumber'
|
|
|
|
|
+import Textarea from 'primevue/textarea'
|
|
|
|
|
+import FileUpload from 'primevue/fileupload'
|
|
|
|
|
+import { useConfirm } from 'primevue/useconfirm'
|
|
|
|
|
+import { useToast } from 'primevue/usetoast'
|
|
|
|
|
+import { listFinance, updateFinance, deleteFinance, updateFinanceStatus, uploadImage } from '@/services/api'
|
|
|
|
|
+import { FinanceStatus } from '@/enums'
|
|
|
|
|
+
|
|
|
|
|
+const toast = useToast()
|
|
|
|
|
+const confirm = useConfirm()
|
|
|
|
|
+
|
|
|
|
|
+// 表格数据
|
|
|
|
|
+const tableData = ref({
|
|
|
|
|
+ content: [],
|
|
|
|
|
+ metadata: {
|
|
|
|
|
+ page: 0,
|
|
|
|
|
+ size: 20,
|
|
|
|
|
+ total: 0
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 加载状态
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// 编辑相关
|
|
|
|
|
+const editDialog = ref(false)
|
|
|
|
|
+const editLoading = ref(false)
|
|
|
|
|
+const uploading = ref(false)
|
|
|
|
|
+const editForm = ref({
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ paymentName: null,
|
|
|
|
|
+ reminderAmount: null,
|
|
|
|
|
+ paymentAccount: null,
|
|
|
|
|
+ bankName: null,
|
|
|
|
|
+ remark: null,
|
|
|
|
|
+ paymentQrCode: null
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 图片上传相关
|
|
|
|
|
+const qrCodePreview = ref(null)
|
|
|
|
|
+const qrCodeFile = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+// 拒绝相关
|
|
|
|
|
+const rejectDialog = ref(false)
|
|
|
|
|
+const rejectLoading = ref(false)
|
|
|
|
|
+const rejectReason = ref('')
|
|
|
|
|
+const currentRejectRecord = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+// 二维码预览
|
|
|
|
|
+const qrCodeDialog = ref(false)
|
|
|
|
|
+const qrCodeImage = ref('')
|
|
|
|
|
+
|
|
|
|
|
+// 搜索表单
|
|
|
|
|
+const searchForm = ref({
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ paymentName: null,
|
|
|
|
|
+ paymentAccount: null,
|
|
|
|
|
+ bankName: null,
|
|
|
|
|
+ status: null,
|
|
|
|
|
+ startDate: null,
|
|
|
|
|
+ endDate: null
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 状态选项
|
|
|
|
|
+const statusOptions = [
|
|
|
|
|
+ { label: '全部', value: null },
|
|
|
|
|
+ { label: FinanceStatus.withdrawn, value: 'withdrawn' },
|
|
|
|
|
+ { label: FinanceStatus.rejected, value: 'rejected' },
|
|
|
|
|
+ { label: FinanceStatus.processing, value: 'processing' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+// 获取状态文本
|
|
|
|
|
+const getStatusText = (status) => {
|
|
|
|
|
+ return FinanceStatus[status] || status
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取状态样式类
|
|
|
|
|
+const getStatusClass = (status) => {
|
|
|
|
|
+ const classMap = {
|
|
|
|
|
+ withdrawn: 'status-withdrawn',
|
|
|
|
|
+ rejected: 'status-rejected',
|
|
|
|
|
+ processing: 'status-processing'
|
|
|
|
|
+ }
|
|
|
|
|
+ return classMap[status] || ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 格式化金额
|
|
|
|
|
+const formatAmount = (amount) => {
|
|
|
|
|
+ if (!amount) return '0.00'
|
|
|
|
|
+ return Number(amount).toFixed(2)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const formatSize = (bytes) => {
|
|
|
|
|
+ const k = 1024
|
|
|
|
|
+ const dm = 3
|
|
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB']
|
|
|
|
|
+
|
|
|
|
|
+ if (bytes === 0) {
|
|
|
|
|
+ return `0 ${sizes[0]}`
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
|
|
+ const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm))
|
|
|
|
|
+
|
|
|
|
|
+ return `${formattedSize} ${sizes[i]}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取数据
|
|
|
|
|
+const fetchData = async () => {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await listFinance(
|
|
|
|
|
+ tableData.value.metadata.page,
|
|
|
|
|
+ tableData.value.metadata.size,
|
|
|
|
|
+ searchForm.value.status || undefined,
|
|
|
|
|
+ searchForm.value.paymentName || undefined,
|
|
|
|
|
+ searchForm.value.startDate ? formatDateForAPI(searchForm.value.startDate) : undefined,
|
|
|
|
|
+ searchForm.value.endDate ? formatDateForAPI(searchForm.value.endDate) : undefined
|
|
|
|
|
+ )
|
|
|
|
|
+ tableData.value = response
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '获取财务记录失败',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 分页处理
|
|
|
|
|
+const handlePageChange = (event) => {
|
|
|
|
|
+ tableData.value.metadata.page = event.page
|
|
|
|
|
+ tableData.value.metadata.size = event.rows
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 搜索处理
|
|
|
|
|
+const handleSearch = () => {
|
|
|
|
|
+ tableData.value.metadata.page = 0
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 刷新处理
|
|
|
|
|
+const handleRefresh = () => {
|
|
|
|
|
+ searchForm.value = {
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ paymentName: null,
|
|
|
|
|
+ paymentAccount: null,
|
|
|
|
|
+ bankName: null,
|
|
|
|
|
+ status: null,
|
|
|
|
|
+ startDate: null,
|
|
|
|
|
+ endDate: null
|
|
|
|
|
+ }
|
|
|
|
|
+ tableData.value.metadata.page = 0
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 格式化日期时间
|
|
|
|
|
+const formatDateTime = (dateString) => {
|
|
|
|
|
+ if (!dateString) return '-'
|
|
|
|
|
+
|
|
|
|
|
+ const date = new Date(dateString)
|
|
|
|
|
+ const year = date.getFullYear()
|
|
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
+ const day = String(date.getDate()).padStart(2, '0')
|
|
|
|
|
+ const hours = String(date.getHours()).padStart(2, '0')
|
|
|
|
|
+ const minutes = String(date.getMinutes()).padStart(2, '0')
|
|
|
|
|
+ const seconds = String(date.getSeconds()).padStart(2, '0')
|
|
|
|
|
+
|
|
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 格式化日期用于API调用
|
|
|
|
|
+const formatDateForAPI = (date) => {
|
|
|
|
|
+ if (!date) return undefined
|
|
|
|
|
+ return useDateFormat(new Date(date), 'YYYY-MM-DD').value
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 确认删除
|
|
|
|
|
+const confirmDelete = (finance) => {
|
|
|
|
|
+ confirm.require({
|
|
|
|
|
+ message: `确定要删除财务记录 "${finance.paymentName}" 吗?`,
|
|
|
|
|
+ header: '确认删除',
|
|
|
|
|
+ icon: 'pi pi-exclamation-triangle',
|
|
|
|
|
+ acceptLabel: '确认',
|
|
|
|
|
+ rejectLabel: '取消',
|
|
|
|
|
+ accept: () => deleteFinanceRecord(finance.id)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 删除财务记录
|
|
|
|
|
+const deleteFinanceRecord = async (id) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await deleteFinance(id)
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '删除成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '删除失败',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 复制到剪贴板
|
|
|
|
|
+const copyToClipboard = async (text) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(text)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ const textArea = document.createElement('textarea')
|
|
|
|
|
+ textArea.value = text
|
|
|
|
|
+ document.body.appendChild(textArea)
|
|
|
|
|
+ textArea.select()
|
|
|
|
|
+ document.execCommand('copy')
|
|
|
|
|
+ document.body.removeChild(textArea)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 打开编辑弹窗
|
|
|
|
|
+const openEditDialog = (finance) => {
|
|
|
|
|
+ editForm.value = {
|
|
|
|
|
+ id: finance.id,
|
|
|
|
|
+ paymentName: finance.paymentName || null,
|
|
|
|
|
+ reminderAmount: finance.reminderAmount || null,
|
|
|
|
|
+ paymentAccount: finance.paymentAccount || null,
|
|
|
|
|
+ bankName: finance.bankName || null,
|
|
|
|
|
+ remark: finance.remark || null,
|
|
|
|
|
+ paymentQrCode: finance.paymentQrCode || null
|
|
|
|
|
+ }
|
|
|
|
|
+ // 编辑时显示原有图片
|
|
|
|
|
+ qrCodePreview.value = finance.paymentQrCode || null
|
|
|
|
|
+ qrCodeFile.value = null
|
|
|
|
|
+ editDialog.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存编辑
|
|
|
|
|
+const saveEdit = async () => {
|
|
|
|
|
+ editLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 准备保存的数据
|
|
|
|
|
+ const saveData = { ...editForm.value }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理图片逻辑
|
|
|
|
|
+ if (qrCodeFile.value) {
|
|
|
|
|
+ // 有新选择的图片文件,先上传
|
|
|
|
|
+ uploading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await uploadImage(qrCodeFile.value)
|
|
|
|
|
+ saveData.paymentQrCode = result.data.url
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: `${result.message || '图片上传成功'} (${formatSize(result.data.size)})`,
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('图片上传失败', error)
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '图片上传失败: ' + (error.message || error),
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ uploading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (qrCodePreview.value === null) {
|
|
|
|
|
+ // 如果图片被移除,设置为null
|
|
|
|
|
+ saveData.paymentQrCode = null
|
|
|
|
|
+ } else if (qrCodePreview.value && !qrCodeFile.value) {
|
|
|
|
|
+ // 编辑时:如果显示原有图片但没有选择新文件,说明图片未修改,删除image参数
|
|
|
|
|
+ delete saveData.paymentQrCode
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await updateFinance(editForm.value.id, saveData)
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '更新成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ editDialog.value = false
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '更新失败',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ editLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 确认通过
|
|
|
|
|
+const confirmApprove = (finance) => {
|
|
|
|
|
+ confirm.require({
|
|
|
|
|
+ message: `确定要通过提现申请吗?`,
|
|
|
|
|
+ header: '提现申请通过',
|
|
|
|
|
+ icon: 'pi pi-check-circle',
|
|
|
|
|
+ acceptLabel: '确认',
|
|
|
|
|
+ rejectLabel: '取消',
|
|
|
|
|
+ accept: () => updateStatus(finance, 'withdrawn')
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 更新状态
|
|
|
|
|
+const updateStatus = async (finance, status) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await updateFinanceStatus(finance.id, { status, rejectReason: '' })
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '状态更新成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '状态更新失败',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 打开拒绝弹窗
|
|
|
|
|
+const openRejectDialog = (finance) => {
|
|
|
|
|
+ currentRejectRecord.value = finance
|
|
|
|
|
+ rejectReason.value = ''
|
|
|
|
|
+ rejectDialog.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 确认拒绝
|
|
|
|
|
+const confirmRejectSubmit = async () => {
|
|
|
|
|
+ rejectLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await updateFinanceStatus(currentRejectRecord.value.id, {
|
|
|
|
|
+ status: 'rejected',
|
|
|
|
|
+ rejectReason: rejectReason.value || ''
|
|
|
|
|
+ })
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '驳回成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ rejectDialog.value = false
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '驳回失败',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ rejectLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 文件选择处理
|
|
|
|
|
+const onQrCodeFileSelect = (event) => {
|
|
|
|
|
+ const file = event.files[0]
|
|
|
|
|
+ if (!file) return
|
|
|
|
|
+
|
|
|
|
|
+ // 检查文件类型
|
|
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '请选择图片文件',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查文件大小 (50MB)
|
|
|
|
|
+ if (file.size > 50000000) {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '图片文件大小不能超过50MB',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const reader = new FileReader()
|
|
|
|
|
+ reader.onload = (e) => {
|
|
|
|
|
+ qrCodePreview.value = e.target.result
|
|
|
|
|
+ qrCodeFile.value = file
|
|
|
|
|
+ }
|
|
|
|
|
+ reader.readAsDataURL(file)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 移除收款码
|
|
|
|
|
+const removeQrCode = () => {
|
|
|
|
|
+ qrCodePreview.value = null
|
|
|
|
|
+ qrCodeFile.value = null
|
|
|
|
|
+ editForm.value.paymentQrCode = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 预览二维码
|
|
|
|
|
+const previewQrCode = (qrCodeData) => {
|
|
|
|
|
+ if (qrCodeData) {
|
|
|
|
|
+ qrCodeImage.value = qrCodeData
|
|
|
|
|
+ qrCodeDialog.value = true
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 初始化
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ fetchData()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.p-datatable-sm .p-datatable-tbody > tr > td {
|
|
|
|
|
+ padding: 0.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.p-datatable-sm .p-datatable-thead > tr > th {
|
|
|
|
|
+ padding: 0.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.finance-table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.finance-table .p-datatable-wrapper {
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.finance-table .p-datatable-thead th {
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ min-width: 100px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.font-mono {
|
|
|
|
|
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.payment-name-text {
|
|
|
|
|
+ max-width: 200px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-text {
|
|
|
|
|
+ color: #059669;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.qr-code-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.qr-code-image {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: transform 0.2s ease;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.qr-code-image:hover {
|
|
|
|
|
+ transform: scale(1.1);
|
|
|
|
|
+ border-color: #3b82f6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.payment-account-text {
|
|
|
|
|
+ max-width: 150px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bank-name-text {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-text {
|
|
|
|
|
+ width: 100px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-withdrawn {
|
|
|
|
|
+ color: #059669;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-rejected {
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-processing {
|
|
|
|
|
+ color: #d97706;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remark-content {
|
|
|
|
|
+ width: 150px;
|
|
|
|
|
+ word-wrap: break-word;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ white-space: normal;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+ max-height: none;
|
|
|
|
|
+ overflow: visible;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remark-text {
|
|
|
|
|
+ color: #059669;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.reject-reason-text {
|
|
|
|
|
+ width: 150px;
|
|
|
|
|
+ word-wrap: break-word;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ white-space: normal;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.font-medium {
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-sm {
|
|
|
|
|
+ font-size: 0.875rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.copyable-text {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.copyable-text:hover {
|
|
|
|
|
+ background-color: #e5e7eb;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.copyable-text:active {
|
|
|
|
|
+ background-color: #d1d5db;
|
|
|
|
|
+ transform: scale(0.98);
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|