Quellcode durchsuchen

更新财务状态API为PUT请求,新增财务视图,支持数据表格展示、搜索、编辑和删除功能。

wuyi vor 4 Monaten
Ursprung
Commit
a5a241e20a
2 geänderte Dateien mit 952 neuen und 1 gelöschten Zeilen
  1. 1 1
      src/services/api.js
  2. 951 0
      src/views/FinanceView.vue

+ 1 - 1
src/services/api.js

@@ -236,7 +236,7 @@ export const updateFinance = async (id, financeData) => {
 
 
 // 更新财务状态
 // 更新财务状态
 export const updateFinanceStatus = async (id, statusData) => {
 export const updateFinanceStatus = async (id, statusData) => {
-  const response = await api.patch(`/finance/${id}/status`, statusData)
+  const response = await api.put(`/finance/${id}/status`, statusData)
   return response.data
   return response.data
 }
 }
 
 

+ 951 - 0
src/views/FinanceView.vue

@@ -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>