Преглед на файлове

feat(purchase): Web管理端购买记录管理

Sisyphus преди 2 месеца
родител
ревизия
27c0cb39eb
променени са 1 файла, в които са добавени 218 реда и са изтрити 0 реда
  1. 218 0
      cfc-web/src/views/admin/PurchaseManagement.vue

+ 218 - 0
cfc-web/src/views/admin/PurchaseManagement.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="purchase-mgmt admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">健康启航计划管理</span>
+        <el-button type="primary" size="mini" @click="loadList" :loading="loading">刷新</el-button>
+      </div>
+
+      <!-- 统计卡片 -->
+      <el-row :gutter="16" style="margin-bottom: 16px;">
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.totalOrders }}</div>
+            <div class="stat-label">总订单数</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.paidOrders }}</div>
+            <div class="stat-label">已支付</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.activeUsers }}</div>
+            <div class="stat-label">活跃体验用户</div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="stat-card">
+            <div class="stat-value">{{ stats.revenue }}</div>
+            <div class="stat-label">总收入(元)</div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <!-- 筛选 -->
+      <el-form :inline="true" size="small" style="margin-bottom: 16px;">
+        <el-form-item label="状态">
+          <el-select v-model="filter.status" placeholder="全部" clearable style="width: 120px;">
+            <el-option label="待支付" value="pending" />
+            <el-option label="已支付" value="paid" />
+            <el-option label="已取消" value="cancelled" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="搜索">
+          <el-input v-model="filter.keyword" placeholder="用户ID/订单号" clearable style="width: 200px;" @keyup.enter.native="loadList" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" size="small" @click="loadList">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 列表 -->
+      <el-table :data="list" v-loading="loading" border stripe style="width: 100%;">
+        <el-table-column prop="orderNo" label="订单号" width="180" />
+        <el-table-column prop="userId" label="用户ID" width="100" />
+        <el-table-column prop="familyId" label="家庭ID" width="100" />
+        <el-table-column prop="amount" label="金额(分)" width="100">
+          <template slot-scope="{ row }">{{ row.amount / 100 }}元</template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="startDate" label="开始时间" width="160">
+          <template slot-scope="{ row }">{{ formatDateTime(row.startDate) }}</template>
+        </el-table-column>
+        <el-table-column prop="endDate" label="结束时间" width="160">
+          <template slot-scope="{ row }">{{ formatDateTime(row.endDate) }}</template>
+        </el-table-column>
+        <el-table-column prop="isEarlyRenewal" label="早期续费" width="90">
+          <template slot-scope="{ row }">
+            <el-tag :type="row.isEarlyRenewal ? 'success' : 'info'" size="small">{{ row.isEarlyRenewal ? '是' : '否' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="创建时间" width="160">
+          <template slot-scope="{ row }">{{ formatDateTime(row.createdAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" size="small" @click="viewDetail(row)">详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <el-pagination
+        style="margin-top: 16px; text-align: right;"
+        layout="total, prev, pager, next"
+        :total="total"
+        :current-page="page"
+        :page-size="size"
+        @current-change="handlePageChange"
+      />
+    </el-card>
+
+    <!-- 详情弹窗 -->
+    <el-dialog title="订单详情" :visible.sync="dialogVisible" width="600px">
+      <el-descriptions :column="2" border size="small" v-if="currentOrder">
+        <el-descriptions-item label="订单号">{{ currentOrder.orderNo }}</el-descriptions-item>
+        <el-descriptions-item label="用户ID">{{ currentOrder.userId }}</el-descriptions-item>
+        <el-descriptions-item label="家庭ID">{{ currentOrder.familyId }}</el-descriptions-item>
+        <el-descriptions-item label="金额">{{ currentOrder.amount / 100 }}元</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusType(currentOrder.status)" size="small">{{ statusText(currentOrder.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="支付方式">{{ currentOrder.paymentMethod || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ formatDateTime(currentOrder.startDate) }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ formatDateTime(currentOrder.endDate) }}</el-descriptions-item>
+        <el-descriptions-item label="是否早期续费">{{ currentOrder.isEarlyRenewal ? '是' : '否' }}</el-descriptions-item>
+        <el-descriptions-item label="已发放CF值">{{ currentOrder.cfRewardGiven || 0 }} CF</el-descriptions-item>
+        <el-descriptions-item label="创建时间">{{ formatDateTime(currentOrder.createdAt) }}</el-descriptions-item>
+        <el-descriptions-item label="支付时间">{{ formatDateTime(currentOrder.paidAt) }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+
+export default {
+  name: 'PurchaseManagement',
+  data() {
+    return {
+      loading: false,
+      list: [],
+      total: 0,
+      page: 1,
+      size: 20,
+      filter: { status: '', keyword: '' },
+      dialogVisible: false,
+      currentOrder: null,
+      stats: {
+        totalOrders: 0,
+        paidOrders: 0,
+        activeUsers: 0,
+        revenue: '0.00'
+      }
+    }
+  },
+  created() {
+    this.loadList()
+    this.loadStats()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const res = await request({
+          url: '/api/admin/purchase/list',
+          method: 'post',
+          data: { page: this.page, size: this.size, ...this.filter }
+        })
+        if (res.data && res.data.code === 200) {
+          this.list = res.data.data.records || []
+          this.total = res.data.data.total || 0
+        }
+      } catch (e) {
+        console.error('加载列表失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadStats() {
+      try {
+        const res = await request({
+          url: '/api/admin/purchase/stats',
+          method: 'post'
+        })
+        if (res.data && res.data.code === 200) {
+          this.stats = res.data.data
+        }
+      } catch (e) {
+        console.error('加载统计失败', e)
+      }
+    },
+    viewDetail(row) {
+      this.currentOrder = row
+      this.dialogVisible = true
+    },
+    handlePageChange(page) {
+      this.page = page
+      this.loadList()
+    },
+    statusType(status) {
+      const map = { pending: 'warning', paid: 'success', cancelled: 'info' }
+      return map[status] || ''
+    },
+    statusText(status) {
+      const map = { pending: '待支付', paid: '已支付', cancelled: '已取消' }
+      return map[status] || status
+    },
+    formatDateTime(dateStr) {
+      if (!dateStr) return '-'
+      const d = new Date(dateStr)
+      if (isNaN(d)) return dateStr.toString().substring(0, 19)
+      return d.getFullYear() + '-' +
+        String(d.getMonth() + 1).padStart(2, '0') + '-' +
+        String(d.getDate()).padStart(2, '0') + ' ' +
+        String(d.getHours()).padStart(2, '0') + ':' +
+        String(d.getMinutes()).padStart(2, '0') + ':' +
+        String(d.getSeconds()).padStart(2, '0')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.admin-page { padding: 16px; }
+.admin-page-header { display: flex; justify-content: space-between; align-items: center; }
+.admin-page-title { font-size: 16px; font-weight: bold; }
+.stat-card { text-align: center; }
+.stat-value { font-size: 28rpx; font-weight: bold; color: #F97316; }
+.stat-label { font-size: 12px; color: #999; margin-top: 4px; }
+</style>