Просмотр исходного кода

新增收入视图,支持数据表格展示、搜索、编辑和删除功能,包含收入类型、订单类型和支付渠道的选择。

wuyi 4 месяцев назад
Родитель
Сommit
1070866052
1 измененных файлов с 777 добавлено и 0 удалено
  1. 777 0
      src/views/IncomeView.vue

+ 777 - 0
src/views/IncomeView.vue

@@ -0,0 +1,777 @@
+<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="income-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.agentName"
+            placeholder="代理"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <Dropdown
+            v-model="searchForm.incomeType"
+            :options="incomeTypeOptions"
+            optionLabel="label"
+            optionValue="value"
+            placeholder="收入类型"
+            size="small"
+            class="w-32"
+            :showClear="true"
+          />
+          <Dropdown
+            v-model="searchForm.orderType"
+            :options="orderTypeOptions"
+            optionLabel="label"
+            optionValue="value"
+            placeholder="订单类型"
+            size="small"
+            class="w-32"
+            :showClear="true"
+          />
+          <Dropdown
+            v-model="searchForm.payChannel"
+            :options="payChannelOptions"
+            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="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="agentName" header="代理" style="min-width: 130px">
+        <template #body="slotProps">
+          <span
+            class="font-medium agent-name-text copyable-text"
+            :title="slotProps.data.agentName"
+            @click="copyToClipboard(slotProps.data.agentName)"
+          >
+            {{ slotProps.data.agentName }}
+          </span>
+        </template>
+      </Column>
+
+      <Column
+        field="incomeAmount"
+        header="收入金额"
+        style="min-width: 100px"
+        :pt="{
+          columnHeaderContent: {
+            class: 'justify-center'
+          }
+        }"
+      >
+        <template #body="slotProps">
+          <span class="amount-text font-semibold">{{ formatAmount(slotProps.data.incomeAmount) }} </span>
+        </template>
+      </Column>
+
+      <Column field="incomeType" header="收入类型" style="min-width: 100px">
+        <template #body="slotProps">
+          <span class="income-type-text" :class="getIncomeTypeClass(slotProps.data.incomeType)">
+            {{ getIncomeTypeText(slotProps.data.incomeType) }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="video" header="影片" style="min-width: 200px; max-width: 260px">
+        <template #body="slotProps">
+          <span
+            class="video-text copyable-text"
+            :title="slotProps.data.video"
+            @click="copyToClipboard(slotProps.data.video)"
+          >
+            {{ slotProps.data.video }}
+          </span>
+        </template>
+      </Column>
+
+      <Column
+        field="price"
+        header="价格"
+        style="min-width: 100px"
+        :pt="{
+          columnHeaderContent: {
+            class: 'justify-center'
+          }
+        }"
+      >
+        <template #body="slotProps">
+          <span class="price-text">{{ formatAmount(slotProps.data.price) }} </span>
+        </template>
+      </Column>
+
+      <Column field="orderType" header="订单类型" style="min-width: 120px">
+        <template #body="slotProps">
+          <span class="order-type-text">
+            {{ getOrderTypeText(slotProps.data.orderType) }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="tipOrderId" header="订单号" style="min-width: 120px">
+        <template #body="slotProps">
+          <span
+            class="font-mono text-sm order-id-text copyable-text"
+            :title="slotProps.data.tipOrderId"
+            @click="copyToClipboard(slotProps.data.tipOrderId)"
+          >
+            {{ slotProps.data.tipOrderId }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="payChannel" header="支付渠道" style="min-width: 100px">
+        <template #body="slotProps">
+          <span class="pay-channel-text">
+            {{ getPayChannelText(slotProps.data.payChannel) }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="payNo" header="支付单号" style="min-width: 120px; max-width: 180px">
+        <template #body="slotProps">
+          <span
+            class="font-mono text-sm pay-no-text copyable-text"
+            :title="slotProps.data.payNo"
+            @click="copyToClipboard(slotProps.data.payNo)"
+          >
+            {{ slotProps.data.payNo }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="source" header="来源" style="min-width: 100px; max-width: 160px">
+        <template #body="slotProps">
+          <span class="source-text">
+            {{ slotProps.data.source }}
+          </span>
+        </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: 150px; width: 150px"
+        align-frozen="right"
+        frozen
+        :pt="{
+          columnHeaderContent: {
+            class: 'justify-center'
+          }
+        }"
+      >
+        <template #body="slotProps">
+          <div class="flex justify-center gap-1">
+            <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="grid grid-cols-2 gap-4">
+          <div class="field">
+            <label for="edit-agentName" class="font-medium text-sm mb-2 block">代理</label>
+            <InputText id="edit-agentName" v-model="editForm.agentName" class="w-full" />
+          </div>
+
+          <div class="field">
+            <label for="edit-incomeAmount" class="font-medium text-sm mb-2 block">收入金额</label>
+            <InputNumber
+              id="edit-incomeAmount"
+              v-model="editForm.incomeAmount"
+              mode="decimal"
+              :min-fraction-digits="2"
+              :max-fraction-digits="2"
+              class="w-full"
+            />
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4 mt-4">
+          <div class="field">
+            <label for="edit-incomeType" class="font-medium text-sm mb-2 block">收入类型</label>
+            <Dropdown
+              id="edit-incomeType"
+              v-model="editForm.incomeType"
+              :options="incomeTypeOptions.filter((option) => option.value !== null)"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择收入类型"
+              class="w-full"
+            />
+          </div>
+
+          <div class="field">
+            <label for="edit-video" class="font-medium text-sm mb-2 block">影片</label>
+            <InputText id="edit-video" v-model="editForm.video" class="w-full" />
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4 mt-4">
+          <div class="field">
+            <label for="edit-price" class="font-medium text-sm mb-2 block">价格</label>
+            <InputNumber
+              id="edit-price"
+              v-model="editForm.price"
+              mode="decimal"
+              :min-fraction-digits="2"
+              :max-fraction-digits="2"
+              class="w-full"
+            />
+          </div>
+
+          <div class="field">
+            <label for="edit-orderType" class="font-medium text-sm mb-2 block">订单类型</label>
+            <Dropdown
+              id="edit-orderType"
+              v-model="editForm.orderType"
+              :options="orderTypeOptions.filter((option) => option.value !== null)"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择订单类型"
+              class="w-full"
+            />
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4 mt-4">
+          <div class="field">
+            <label for="edit-tipOrderId" class="font-medium text-sm mb-2 block">订单号</label>
+            <InputText id="edit-tipOrderId" v-model="editForm.tipOrderId" class="w-full" />
+          </div>
+
+          <div class="field">
+            <label for="edit-payChannel" class="font-medium text-sm mb-2 block">支付渠道</label>
+            <Dropdown
+              id="edit-payChannel"
+              v-model="editForm.payChannel"
+              :options="payChannelOptions.filter((option) => option.value !== null)"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择支付渠道"
+              class="w-full"
+            />
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4 mt-4">
+          <div class="field">
+            <label for="edit-payNo" class="font-medium text-sm mb-2 block">支付单号</label>
+            <InputText id="edit-payNo" v-model="editForm.payNo" class="w-full" />
+          </div>
+
+          <div class="field">
+            <label for="edit-source" class="font-medium text-sm mb-2 block">来源</label>
+            <InputText id="edit-source" v-model="editForm.source" class="w-full" />
+          </div>
+        </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>
+  </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 { useConfirm } from 'primevue/useconfirm'
+import { useToast } from 'primevue/usetoast'
+import { listIncome, updateIncome, deleteIncome } from '@/services/api'
+import { IncomeType, OrderType } 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 editForm = ref({
+  id: null,
+  agentName: null,
+  incomeAmount: null,
+  incomeType: null,
+  orderType: null,
+  price: null,
+  tipOrderId: null,
+  payChannel: null,
+  payNo: null,
+  video: null,
+  source: null
+})
+
+// 搜索表单
+const searchForm = ref({
+  id: null,
+  agentName: null,
+  incomeType: null,
+  orderType: null,
+  payChannel: null,
+  startDate: null,
+  endDate: null
+})
+
+// 收入类型选项
+const incomeTypeOptions = [
+  { label: '全部', value: null },
+  { label: IncomeType.tip, value: 'tip' },
+  { label: IncomeType.commission, value: 'commission' }
+]
+
+// 订单类型选项
+const orderTypeOptions = [
+  { label: '全部', value: null },
+  { label: OrderType.single_tip, value: 'single_tip' },
+  { label: OrderType.hourly_member, value: 'hourly_member' },
+  { label: OrderType.weekly_member, value: 'weekly_member' },
+  { label: OrderType.monthly_member, value: 'monthly_member' },
+  { label: OrderType.yearly_member, value: 'yearly_member' },
+  { label: OrderType.lifetime_member, value: 'lifetime_member' }
+]
+
+// 支付渠道选项
+const payChannelOptions = [
+  { label: '全部', value: null },
+  { label: '支付宝', value: 'alipay' },
+  { label: '微信支付', value: 'wechat' },
+  { label: '银行卡', value: 'bank' }
+]
+
+// 获取收入类型文本
+const getIncomeTypeText = (type) => {
+  return IncomeType[type] || type
+}
+
+// 获取收入类型样式类
+const getIncomeTypeClass = (type) => {
+  const classMap = {
+    tip: 'income-type-tip',
+    commission: 'income-type-commission'
+  }
+  return classMap[type] || ''
+}
+
+// 获取订单类型文本
+const getOrderTypeText = (type) => {
+  return OrderType[type] || type
+}
+
+// 获取支付渠道文本
+const getPayChannelText = (channel) => {
+  const channelMap = {
+    alipay: '支付宝',
+    wechat: '微信支付',
+    bank: '银行卡'
+  }
+  return channelMap[channel] || channel
+}
+
+// 格式化金额
+const formatAmount = (amount) => {
+  if (!amount) return '0.00'
+  return Number(amount).toFixed(2)
+}
+
+// 格式化日期时间
+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}`
+}
+
+// 获取数据
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const response = await listIncome(
+      tableData.value.metadata.page,
+      tableData.value.metadata.size,
+      searchForm.value.agentName || undefined,
+      searchForm.value.incomeType || 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,
+    agentName: null,
+    incomeType: null,
+    orderType: null,
+    payChannel: null,
+    startDate: null,
+    endDate: null
+  }
+  tableData.value.metadata.page = 0
+  fetchData()
+}
+
+// 格式化日期用于API调用
+const formatDateForAPI = (date) => {
+  if (!date) return undefined
+  return useDateFormat(new Date(date), 'YYYY-MM-DD').value
+}
+
+// 确认删除
+const confirmDelete = (income) => {
+  confirm.require({
+    message: `确定要删除收入记录 "${income.agentName}" 吗?`,
+    header: '确认删除',
+    icon: 'pi pi-exclamation-triangle',
+    accept: () => deleteIncomeRecord(income.id)
+  })
+}
+
+// 删除收入记录
+const deleteIncomeRecord = async (id) => {
+  try {
+    await deleteIncome(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 = (income) => {
+  editForm.value = {
+    id: income.id,
+    agentName: income.agentName || null,
+    incomeAmount: income.incomeAmount || null,
+    incomeType: income.incomeType || null,
+    orderType: income.orderType || null,
+    price: income.price || null,
+    tipOrderId: income.tipOrderId || null,
+    payChannel: income.payChannel || null,
+    payNo: income.payNo || null,
+    video: income.video || null,
+    source: income.source || null
+  }
+  editDialog.value = true
+}
+
+// 保存编辑
+const saveEdit = async () => {
+  editLoading.value = true
+  try {
+    await updateIncome(editForm.value.id, editForm.value)
+    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
+  }
+}
+
+// 初始化
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<style scoped>
+.p-datatable-sm .p-datatable-tbody > tr > td {
+  padding: 0.5rem;
+  vertical-align: top;
+}
+
+.p-datatable-sm .p-datatable-thead > tr > th {
+  padding: 0.5rem;
+}
+
+.income-table {
+  width: 100%;
+}
+
+.income-table .p-datatable-wrapper {
+  overflow-x: auto;
+}
+
+.income-table .p-datatable-thead th {
+  white-space: nowrap;
+  min-width: 100px;
+}
+
+.font-mono {
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.agent-name-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.amount-text {
+  color: #059669;
+  font-weight: 600;
+}
+
+.income-type-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.income-type-tip {
+  color: #7c3aed;
+  font-weight: 500;
+}
+
+.income-type-commission {
+  color: #f59e0b;
+  font-weight: 500;
+}
+
+.order-type-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.order-id-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.price-text {
+  color: #6b7280;
+  font-weight: 500;
+}
+
+.pay-channel-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.pay-no-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.video-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.source-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.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>