Jelajahi Sumber

添加分享记录后台页面

wilhelm wong 2 bulan lalu
induk
melakukan
36eb8f58f0
4 mengubah file dengan 785 tambahan dan 0 penghapusan
  1. 5 0
      src/router/index.js
  2. 48 0
      src/services/api.js
  3. 6 0
      src/views/MainView.vue
  4. 726 0
      src/views/UserShareView.vue

+ 5 - 0
src/router/index.js

@@ -78,6 +78,11 @@ const router = createRouter({
           name: 'link',
           name: 'link',
           component: () => import('@/views/LinkView.vue')
           component: () => import('@/views/LinkView.vue')
         },
         },
+        {
+          path: 'user-share',
+          name: 'user-share',
+          component: () => import('@/views/UserShareView.vue')
+        },
       ]
       ]
     },
     },
     {
     {

+ 48 - 0
src/services/api.js

@@ -537,4 +537,52 @@ export const getTeamDomainAllStatistics = async (domain) => {
   if (domain) params.domain = domain
   if (domain) params.domain = domain
   const response = await api.get('/team-domains/statistics/all', { params })
   const response = await api.get('/team-domains/statistics/all', { params })
   return response.data
   return response.data
+}
+
+// ==================== 用户分享记录相关API ====================
+
+// 获取用户分享记录列表
+export const listUserShareRecords = async (params) => {
+  const response = await api.get('/user-share/records', { params })
+  return response.data
+}
+
+// 获取用户分享统计数据
+export const getUserShareStatistics = async (params) => {
+  const response = await api.get('/user-share/stats', { params })
+  return response.data
+}
+
+// 获取单个用户分享记录
+export const getUserShareRecord = async (id) => {
+  const response = await api.get(`/user-share/records/${id}`)
+  return response.data
+}
+
+// 删除用户分享记录
+export const deleteUserShareRecord = async (id) => {
+  const response = await api.delete(`/user-share/records/${id}`)
+  return response.data
+}
+
+// 批量删除用户分享记录
+export const batchDeleteUserShareRecords = async (ids) => {
+  const response = await api.post('/user-share/records/batch-delete', { ids })
+  return response.data
+}
+
+// 导出用户分享记录
+export const exportUserShareRecords = async (startDate, endDate, username, teamId, status) => {
+  const params = {}
+  if (startDate) params.startDate = startDate
+  if (endDate) params.endDate = endDate
+  if (username) params.username = username
+  if (teamId) params.teamId = teamId
+  if (status !== null && status !== undefined) params.status = status
+
+  const response = await api.get('/user-share/records/export', { 
+    params,
+    responseType: 'blob'
+  })
+  return response.data
 }
 }

+ 6 - 0
src/views/MainView.vue

@@ -72,6 +72,12 @@ const allNavItems = [
     name: 'link',
     name: 'link',
     roles: ['admin', 'team', 'promoter']
     roles: ['admin', 'team', 'promoter']
   },
   },
+  {
+    label: '用户分享记录',
+    icon: 'pi pi-fw pi-share-alt',
+    name: 'user-share',
+    roles: ['admin', 'team']
+  },
   {
   {
     label: '财务记录',
     label: '财务记录',
     icon: 'pi pi-fw pi-credit-card',
     icon: 'pi pi-fw pi-credit-card',

+ 726 - 0
src/views/UserShareView.vue

@@ -0,0 +1,726 @@
+<template>
+  <div class="user-share-view">
+    <!-- 统计信息 -->
+    <div class="stats-section" v-if="statistics">
+      <div class="stats-grid">
+        <div class="stat-card">
+          <div class="stat-icon">
+            <i class="pi pi-share-alt"></i>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.totalShares || 0 }}</div>
+            <div class="stat-label">总分享数</div>
+          </div>
+        </div>
+        <div class="stat-card">
+          <div class="stat-icon">
+            <i class="pi pi-check-circle"></i>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.successShares || 0 }}</div>
+            <div class="stat-label">成功分享</div>
+          </div>
+        </div>
+        <div class="stat-card">
+          <div class="stat-icon">
+            <i class="pi pi-calendar"></i>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.todayShares || 0 }}</div>
+            <div class="stat-label">今日分享</div>
+          </div>
+        </div>
+        <div class="stat-card">
+          <div class="stat-icon">
+            <i class="pi pi-gift"></i>
+          </div>
+          <div class="stat-content">
+            <div class="stat-value">{{ statistics.rewardCount || 0 }}</div>
+            <div class="stat-label">奖励发放</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 数据表格 -->
+    <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
+      <DataTable
+        :value="tableData.content || []"
+        :loading="loading"
+        :paginator="true"
+        :rows="tableData.metadata?.size || 20"
+        :totalRecords="tableData.metadata?.total || 0"
+        :lazy="true"
+        @page="handlePageChange"
+        :sortField="sortField"
+        :sortOrder="sortOrder"
+        @sort="handleSort"
+        paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
+        :rowsPerPageOptions="[10, 20, 50, 100]"
+        currentPageReportTemplate="显示 {first} 到 {last} 条,共 {totalRecords} 条记录"
+        responsiveLayout="scroll"
+        class="p-datatable-sm"
+        :first="(tableData.metadata?.page || 0) * (tableData.metadata?.size || 20)"
+      >
+        <template #header>
+          <div class="flex flex-wrap items-center gap-2">
+            <Calendar
+              v-model="filters.startDate"
+              dateFormat="yy-mm-dd"
+              placeholder="开始日期"
+              showIcon
+              size="small"
+              class="w-32"
+              :maxDate="filters.endDate"
+            />
+            <Calendar
+              v-model="filters.endDate"
+              dateFormat="yy-mm-dd"
+              placeholder="结束日期"
+              showIcon
+              size="small"
+              class="w-32"
+              :minDate="filters.startDate"
+            />
+            <InputText
+              v-model="filters.inviterName"
+              placeholder="邀请者用户名"
+              size="small"
+              class="w-32"
+              @keyup.enter="handleSearch"
+            />
+            <Dropdown
+              v-model="filters.teamId"
+              :options="teamOptions"
+              optionLabel="name"
+              optionValue="id"
+              placeholder="选择团队"
+              showClear
+              size="small"
+              class="w-32"
+            />
+            <Dropdown
+              v-model="filters.status"
+              :options="statusOptions"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择状态"
+              showClear
+              size="small"
+              class="w-32"
+            />
+            <Button
+              icon="pi pi-search"
+              @click="handleSearch"
+              label="搜索"
+              size="small"
+              severity="secondary"
+              :loading="loading"
+            />
+            <Button
+              icon="pi pi-refresh"
+              @click="handleReset"
+              label="重置"
+              size="small"
+            />
+            <Button
+              icon="pi pi-download"
+              @click="handleExport"
+              label="导出"
+              size="small"
+              severity="success"
+              :disabled="!tableData.content?.length"
+            />
+            <div class="flex-1"></div>
+          </div>
+        </template>
+
+        <Column field="id" header="ID" :sortable="true" style="width: 80px">
+          <template #body="{ data }">
+            <span class="font-mono">{{ data.id }}</span>
+          </template>
+        </Column>
+        <Column field="inviterName" header="分享者" :sortable="true" style="width: 120px">
+          <template #body="{ data }">
+            <div class="user-info">
+              <div class="user-name">{{ data.inviterName || '未知用户' }}</div>
+              <div class="user-id">ID: {{ data.inviterId }}</div>
+            </div>
+          </template>
+        </Column>
+        <Column field="teamName" header="团队" :sortable="true" style="width: 120px">
+          <template #body="{ data }">
+            <div class="team-info" v-if="data.teamName">
+              <div class="team-name">{{ data.teamName }}</div>
+              <div class="team-id">ID: {{ data.teamId }}</div>
+            </div>
+            <span v-else class="text-muted">无团队</span>
+          </template>
+        </Column>
+        <Column field="resourceId" header="资源ID" :sortable="true" style="width: 150px">
+          <template #body="{ data }">
+            <span class="font-mono">{{ data.resourceId }}</span>
+          </template>
+        </Column>
+        <Column field="status" header="状态" :sortable="true" style="width: 100px">
+          <template #body="{ data }">
+            <Tag
+              :value="data.status ? '成功' : '失败'"
+              :severity="data.status ? 'success' : 'danger'"
+            />
+          </template>
+        </Column>
+        <Column field="invitedUserIp" header="IP地址" style="width: 120px">
+          <template #body="{ data }">
+            <span class="font-mono">{{ data.invitedUserIp || '-' }}</span>
+          </template>
+        </Column>
+        <Column field="createdAt" header="分享时间" :sortable="true" style="width: 150px">
+          <template #body="{ data }">
+            <span>{{ formatDate(data.createdAt) }}</span>
+          </template>
+        </Column>
+        <Column header="操作" style="width: 120px" :frozen="true" alignFrozen="right">
+          <template #body="{ data }">
+            <div class="action-buttons">
+              <Button
+                icon="pi pi-eye"
+                size="small"
+                severity="info"
+                text
+                @click="viewDetails(data)"
+                v-tooltip.top="'查看详情'"
+              />
+              <Button
+                icon="pi pi-trash"
+                size="small"
+                severity="danger"
+                text
+                @click="deleteRecord(data)"
+                v-tooltip.top="'删除记录'"
+                v-if="userStore.userInfo.role === 'admin'"
+              />
+            </div>
+          </template>
+        </Column>
+      </DataTable>
+    </div>
+
+    <!-- 详情对话框 -->
+    <Dialog
+      v-model:visible="detailDialog.visible"
+      :header="`分享记录详情 - #${detailDialog.data?.id}`"
+      :style="{ width: '600px' }"
+      :modal="true"
+    >
+      <div v-if="detailDialog.data" class="detail-content">
+        <div class="detail-section">
+          <h4>基本信息</h4>
+          <div class="detail-grid">
+            <div class="detail-item">
+              <label>记录ID</label>
+              <span>{{ detailDialog.data.id }}</span>
+            </div>
+            <div class="detail-item">
+              <label>分享者</label>
+              <span>{{ detailDialog.data.inviterName || '未知用户' }} (ID: {{ detailDialog.data.inviterId }})</span>
+            </div>
+            <div class="detail-item" v-if="detailDialog.data.teamName">
+              <label>团队</label>
+              <span>{{ detailDialog.data.teamName }} (ID: {{ detailDialog.data.teamId }})</span>
+            </div>
+            <div class="detail-item">
+              <label>资源ID</label>
+              <span class="font-mono">{{ detailDialog.data.resourceId }}</span>
+            </div>
+            <div class="detail-item">
+              <label>状态</label>
+              <Tag
+                :value="detailDialog.data.status ? '成功' : '失败'"
+                :severity="detailDialog.data.status ? 'success' : 'danger'"
+              />
+            </div>
+            <div class="detail-item">
+              <label>IP地址</label>
+              <span class="font-mono">{{ detailDialog.data.invitedUserIp || '-' }}</span>
+            </div>
+            <div class="detail-item">
+              <label>分享时间</label>
+              <span>{{ formatDate(detailDialog.data.createdAt) }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import { useToast } from 'primevue/usetoast'
+import { useUserStore } from '@/stores/user'
+import { useTeamStore } from '@/stores/team'
+import Button from 'primevue/button'
+import DataTable from 'primevue/datatable'
+import Column from 'primevue/column'
+import Tag from 'primevue/tag'
+import Calendar from 'primevue/calendar'
+import InputText from 'primevue/inputtext'
+import Dropdown from 'primevue/dropdown'
+import Dialog from 'primevue/dialog'
+import {
+  listUserShareRecords,
+  getUserShareStatistics,
+  deleteUserShareRecord
+} from '@/services/api'
+
+const toast = useToast()
+const userStore = useUserStore()
+const teamStore = useTeamStore()
+
+// 响应式数据
+const loading = ref(false)
+const tableData = ref({
+  content: [],
+  metadata: {
+    page: 0,
+    size: 20,
+    total: 0
+  }
+})
+const statistics = ref(null)
+
+// 筛选条件
+const filters = reactive({
+  startDate: null,
+  endDate: null,
+  inviterName: '',
+  teamId: null,
+  status: null
+})
+
+// 排序信息
+const sortField = ref('createdAt')
+const sortOrder = ref(-1)
+
+// 状态选项
+const statusOptions = [
+  { label: '全部', value: null },
+  { label: '成功', value: true },
+  { label: '失败', value: false }
+]
+
+// 团队选项
+const teamOptions = computed(() => {
+  return teamStore.teams.map(team => ({
+    id: team.id,
+    name: team.name
+  }))
+})
+
+// 详情对话框
+const detailDialog = reactive({
+  visible: false,
+  data: null
+})
+
+// 格式化日期
+const formatDate = (dateString) => {
+  if (!dateString) return '-'
+  const date = new Date(dateString)
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  })
+}
+
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: tableData.value.metadata.page,
+      size: tableData.value.metadata.size,
+      sortField: sortField.value,
+      sortOrder: sortOrder.value
+    }
+
+    // 添加筛选条件
+    if (filters.startDate) {
+      params.startDate = filters.startDate.toISOString().split('T')[0]
+    }
+    if (filters.endDate) {
+      params.endDate = filters.endDate.toISOString().split('T')[0]
+    }
+    if (filters.inviterName) {
+      params.inviterName = filters.inviterName
+    }
+    if (filters.teamId) {
+      params.teamId = filters.teamId
+    }
+    if (filters.status !== null) {
+      params.status = filters.status
+    }
+
+    // 为统计API创建单独的参数对象,不包含分页参数
+    const statsParams = {}
+    if (filters.startDate) {
+      statsParams.startDate = filters.startDate.toISOString().split('T')[0]
+    }
+    if (filters.endDate) {
+      statsParams.endDate = filters.endDate.toISOString().split('T')[0]
+    }
+    if (filters.inviterName) {
+      statsParams.inviterName = filters.inviterName
+    }
+    if (filters.teamId) {
+      statsParams.teamId = filters.teamId
+    }
+    if (filters.status !== null) {
+      statsParams.status = filters.status
+    }
+
+    const [recordsData, statsData] = await Promise.all([
+      listUserShareRecords(params),
+      getUserShareStatistics(statsParams)
+    ])
+
+    tableData.value = recordsData
+    statistics.value = statsData
+  } catch (error) {
+    console.error('加载数据失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '加载数据失败',
+      life: 3000
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+// 搜索
+const handleSearch = () => {
+  tableData.value.metadata.page = 0
+  loadData()
+}
+
+// 重置筛选条件
+const handleReset = () => {
+  Object.assign(filters, {
+    startDate: null,
+    endDate: null,
+    inviterName: '',
+    teamId: null,
+    status: null
+  })
+  tableData.value.metadata.page = 0
+  loadData()
+}
+
+// 分页变化
+const handlePageChange = (event) => {
+  console.log('分页变化:', event)
+  tableData.value.metadata.page = event.page
+  tableData.value.metadata.size = event.rows
+  loadData()
+}
+
+// 排序变化
+const handleSort = (event) => {
+  sortField.value = event.sortField
+  sortOrder.value = event.sortOrder === 1 ? 1 : -1
+  loadData()
+}
+
+// 查看详情
+const viewDetails = (record) => {
+  detailDialog.data = record
+  detailDialog.visible = true
+}
+
+// 删除记录
+const deleteRecord = async (record) => {
+  if (!confirm(`确定要删除分享记录 #${record.id} 吗?`)) {
+    return
+  }
+
+  try {
+    await deleteUserShareRecord(record.id)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '删除成功',
+      life: 2000
+    })
+    loadData()
+  } catch (error) {
+    console.error('删除失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '删除失败',
+      life: 3000
+    })
+  }
+}
+
+// 导出数据
+const handleExport = () => {
+  // TODO: 实现导出功能
+  toast.add({
+    severity: 'info',
+    summary: '提示',
+    detail: '导出功能开发中',
+    life: 2000
+  })
+}
+
+// 组件挂载时加载数据
+onMounted(() => {
+  loadData()
+})
+</script>
+
+<style scoped>
+.user-share-view {
+  padding: 1rem;
+}
+
+.page-header {
+  margin-bottom: 2rem;
+}
+
+.page-title {
+  font-size: 1.5rem;
+  font-weight: 600;
+  margin: 0 0 0.5rem 0;
+  color: var(--p-text-color);
+}
+
+.page-description {
+  color: var(--p-text-color-secondary);
+  margin: 0;
+}
+
+.stats-section {
+  margin-bottom: 1.5rem;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 1rem;
+}
+
+.stat-card {
+  background: var(--p-surface-card);
+  border-radius: 8px;
+  padding: 1.5rem;
+  border: 1px solid var(--p-surface-border);
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.stat-icon {
+  width: 3rem;
+  height: 3rem;
+  border-radius: 50%;
+  background: var(--p-primary-100);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--p-primary-600);
+  font-size: 1.25rem;
+}
+
+.stat-content {
+  flex: 1;
+}
+
+.stat-value {
+  font-size: 1.5rem;
+  font-weight: 600;
+  color: var(--p-text-color);
+  line-height: 1;
+}
+
+.stat-label {
+  color: var(--p-text-color-secondary);
+  font-size: 0.875rem;
+  margin-top: 0.25rem;
+}
+
+.table-section {
+  background: var(--p-surface-card);
+  border-radius: 8px;
+  border: 1px solid var(--p-surface-border);
+  overflow: hidden;
+}
+
+.user-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+}
+
+.user-name {
+  font-weight: 500;
+  color: var(--p-text-color);
+}
+
+.user-id {
+  font-size: 0.75rem;
+  color: var(--p-text-color-secondary);
+}
+
+.team-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+}
+
+.team-name {
+  font-weight: 500;
+  color: var(--p-text-color);
+}
+
+.team-id {
+  font-size: 0.75rem;
+  color: var(--p-text-color-secondary);
+}
+
+.action-buttons {
+  display: flex;
+  gap: 0.25rem;
+}
+
+.detail-content {
+  padding: 1rem 0;
+}
+
+.detail-section {
+  margin-bottom: 1.5rem;
+}
+
+.detail-section h4 {
+  margin: 0 0 1rem 0;
+  color: var(--p-text-color);
+  font-size: 1rem;
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+  gap: 1rem;
+}
+
+.detail-item {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+}
+
+.detail-item label {
+  font-weight: 500;
+  color: var(--p-text-color-secondary);
+  font-size: 0.875rem;
+}
+
+.detail-item span {
+  color: var(--p-text-color);
+}
+
+.text-muted {
+  color: var(--p-text-color-secondary);
+}
+
+.font-mono {
+  font-family: 'Courier New', monospace;
+  font-size: 0.875rem;
+}
+
+/* 移动端响应式设计 */
+@media (max-width: 768px) {
+  .user-share-view {
+    padding: 0.5rem;
+  }
+  
+  .page-header {
+    margin-bottom: 1rem;
+  }
+  
+  .page-title {
+    font-size: 1.25rem;
+  }
+  
+  .stats-grid {
+    grid-template-columns: 1fr;
+    gap: 0.75rem;
+  }
+  
+  .stat-card {
+    padding: 1rem;
+  }
+  
+  .detail-grid {
+    grid-template-columns: 1fr;
+  }
+  
+  /* 移动端表格优化 */
+  .p-datatable {
+    font-size: 0.875rem;
+  }
+  
+  .p-datatable .p-datatable-header {
+    padding: 0.5rem;
+  }
+  
+  .p-datatable .p-datatable-tbody > tr > td {
+    padding: 0.5rem;
+  }
+  
+  /* 移动端按钮优化 */
+  .action-buttons {
+    flex-direction: column;
+    gap: 0.25rem;
+  }
+  
+  .action-buttons .p-button {
+    width: 100%;
+    justify-content: center;
+  }
+}
+
+/* 小屏幕移动端进一步优化 */
+@media (max-width: 480px) {
+  .user-share-view {
+    padding: 0.25rem;
+  }
+  
+  .page-title {
+    font-size: 1.125rem;
+  }
+  
+  .stat-card {
+    padding: 0.75rem;
+  }
+  
+  .stat-icon {
+    width: 2.5rem;
+    height: 2.5rem;
+    font-size: 1rem;
+  }
+  
+  .stat-value {
+    font-size: 1.25rem;
+  }
+}
+</style>