瀏覽代碼

新视频源

wilhelm wong 3 周之前
父節點
當前提交
e57f50ad43

+ 1 - 0
package.json

@@ -24,6 +24,7 @@
     "primeflex": "^4.0.0",
     "primeflex": "^4.0.0",
     "primeicons": "^7.0.0",
     "primeicons": "^7.0.0",
     "primevue": "^4.3.3",
     "primevue": "^4.3.3",
+    "qrcode": "^1.5.4",
     "tailwindcss-primeui": "^0.6.1",
     "tailwindcss-primeui": "^0.6.1",
     "url": "^0.11.4",
     "url": "^0.11.4",
     "v-viewer": "^3.0.22",
     "v-viewer": "^3.0.22",

+ 5 - 0
src/router/index.js

@@ -98,6 +98,11 @@ const router = createRouter({
           name: 'domain-management',
           name: 'domain-management',
           component: () => import('@/views/DomainManagementView.vue')
           component: () => import('@/views/DomainManagementView.vue')
         },
         },
+        {
+          path: 'video-feedback',
+          name: 'video-feedback',
+          component: () => import('@/views/VideoFeedbackView.vue')
+        },
       ]
       ]
     },
     },
     {
     {

+ 73 - 0
src/services/api.js

@@ -109,6 +109,16 @@ export const getSysConfigByName = async (name) => {
   return response.data
   return response.data
 }
 }
 
 
+// 获取域名列表配置(从 sysconfig 读取 domain__redirect 和 domain_land)
+export const getConfigDomains = async (teamId) => {
+  const params = {}
+  if (teamId !== null && teamId !== undefined) {
+    params.teamId = teamId
+  }
+  const response = await api.get('/config/domains', { params })
+  return response.data
+}
+
 
 
 // 文件上传API
 // 文件上传API
 export const uploadFile = async (file) => {
 export const uploadFile = async (file) => {
@@ -351,6 +361,12 @@ export const getCurrentUserTeam = async () => {
   return response.data
   return response.data
 }
 }
 
 
+// 生成一级代理链接
+export const generateFirstLevelAgentLink = async (teamId) => {
+  const response = await api.post(`/teams/${teamId}/generate-first-level-agent-link`)
+  return response.data
+}
+
 
 
 // ==================== 团队成员管理相关API ====================
 // ==================== 团队成员管理相关API ====================
 
 
@@ -867,4 +883,61 @@ export const deleteDomainManagement = async (id) => {
 export const getDomainManagementTypes = async () => {
 export const getDomainManagementTypes = async () => {
   const response = await api.get('/domain-management/types/all')
   const response = await api.get('/domain-management/types/all')
   return response.data
   return response.data
+}
+
+// ==================== 提现密码相关API ====================
+
+// 检查团队提现密码状态
+export const checkTeamWithdrawPasswordStatus = async () => {
+  const response = await api.get('/teams/withdraw-password/status')
+  return response.data
+}
+
+// 修改团队提现密码
+export const updateTeamWithdrawPassword = async (oldPassword, newPassword) => {
+  const body = { newPassword }
+  if (oldPassword) {
+    body.oldPassword = oldPassword
+  }
+  const response = await api.put('/teams/withdraw-password', body)
+  return response.data
+}
+
+// 检查团队成员提现密码状态
+export const checkTeamMemberWithdrawPasswordStatus = async () => {
+  const response = await api.get('/team-members/withdraw-password/status')
+  return response.data
+}
+
+// 修改团队成员提现密码
+export const updateTeamMemberWithdrawPassword = async (oldPassword, newPassword) => {
+  const body = { newPassword }
+  if (oldPassword) {
+    body.oldPassword = oldPassword
+  }
+  const response = await api.put('/team-members/withdraw-password', body)
+  return response.data
+}
+
+// ==================== 视频反馈相关API ====================
+
+// 提交视频反馈
+export const submitVideoFeedback = async (videoId, reason, url) => {
+  const body = {
+    videoId,
+    reason
+  }
+  if (url) {
+    body.url = url
+  }
+  const response = await api.post('/video-feedback/submit', body)
+  return response.data
+}
+
+// 获取视频反馈列表
+export const listVideoFeedback = async (page = 0, size = 20) => {
+  const response = await api.get('/video-feedback/list', {
+    params: { page, size }
+  })
+  return response.data
 }
 }

+ 22 - 3
src/views/DashboardView.vue

@@ -38,6 +38,11 @@
           <p class="stat-value new-user-value">{{ teamStats?.todayNewUsers || 0 }}</p>
           <p class="stat-value new-user-value">{{ teamStats?.todayNewUsers || 0 }}</p>
           <p class="stat-label">新注册用户</p>
           <p class="stat-label">新注册用户</p>
         </div>
         </div>
+        <div class="card balance-card">
+          <h3>余额</h3>
+          <p class="stat-value balance-value">{{ formatAmount(teamStats?.balance) }}</p>
+          <p class="stat-label">可提现金额</p>
+        </div>
       </div>
       </div>
 
 
       <!-- 队长视图 - 隐藏原来的内容 -->
       <!-- 队长视图 - 隐藏原来的内容 -->
@@ -214,6 +219,10 @@
                 <span class="text-gray-500 text-sm mb-1">总用户数</span>
                 <span class="text-gray-500 text-sm mb-1">总用户数</span>
                 <span class="text-lg font-extrabold text-teal-600">{{ teamData.totalUsers || 0 }}</span>
                 <span class="text-lg font-extrabold text-teal-600">{{ teamData.totalUsers || 0 }}</span>
               </div>
               </div>
+              <div class="flex flex-col">
+                <span class="text-gray-500 text-sm mb-1">余额</span>
+                <span class="text-lg font-extrabold text-purple-700">¥{{ formatAmount(teamData.balance) }}</span>
+              </div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -618,7 +627,8 @@ const teamData = computed(() => {
           todaySales: teamStats.value.todaySales || baseTeamInfo.todaySales || 0,
           todaySales: teamStats.value.todaySales || baseTeamInfo.todaySales || 0,
           totalSales: teamStats.value.totalSales || baseTeamInfo.totalSales || 0,
           totalSales: teamStats.value.totalSales || baseTeamInfo.totalSales || 0,
           todayDAU: teamStats.value.todayDAU || baseTeamInfo.todayDAU || 0,
           todayDAU: teamStats.value.todayDAU || baseTeamInfo.todayDAU || 0,
-          totalUsers: teamStats.value.totalUsers || baseTeamInfo.totalUsers || 0
+          totalUsers: teamStats.value.totalUsers || baseTeamInfo.totalUsers || 0,
+          balance: teamStats.value.balance || baseTeamInfo.balance || 0
         }
         }
       }
       }
       return baseTeamInfo
       return baseTeamInfo
@@ -637,7 +647,8 @@ const teamData = computed(() => {
         totalRevenue: teamStats.value.totalRevenue || 0,
         totalRevenue: teamStats.value.totalRevenue || 0,
         todaySales: teamStats.value.todaySales || 0,
         todaySales: teamStats.value.todaySales || 0,
         totalSales: teamStats.value.totalSales || 0,
         totalSales: teamStats.value.totalSales || 0,
-        todayDAU: teamStats.value.todayDAU || 0
+        todayDAU: teamStats.value.todayDAU || 0,
+        balance: teamStats.value.balance || 0
       }
       }
     }
     }
   }
   }
@@ -1429,7 +1440,8 @@ onUnmounted(() => {
 .revenue-card,
 .revenue-card,
 .sales-card,
 .sales-card,
 .user-card,
 .user-card,
-.new-user-card {
+.new-user-card,
+.balance-card {
   border-left: none;
   border-left: none;
 }
 }
 
 
@@ -1441,6 +1453,10 @@ onUnmounted(() => {
   color: #8b5cf6;
   color: #8b5cf6;
 }
 }
 
 
+.balance-value {
+  color: #7c3aed;
+}
+
 .team-selector {
 .team-selector {
   grid-column: 1 / -1;
   grid-column: 1 / -1;
   margin-top: 1rem;
   margin-top: 1rem;
@@ -1752,6 +1768,7 @@ onUnmounted(() => {
 .icon-orange { background: #ffedd5; border: 1px solid #fed7aa; color: #ea580c; }
 .icon-orange { background: #ffedd5; border: 1px solid #fed7aa; color: #ea580c; }
 .icon-indigo { background: #e0e7ff; border: 1px solid #c7d2fe; color: #4f46e5; }
 .icon-indigo { background: #e0e7ff; border: 1px solid #c7d2fe; color: #4f46e5; }
 .icon-slate { background: #f1f5f9; border: 1px solid #e2e8f0; color: #475569; }
 .icon-slate { background: #f1f5f9; border: 1px solid #e2e8f0; color: #475569; }
+.icon-purple { background: #f3e8ff; border: 1px solid #e9d5ff; color: #7c3aed; }
 
 
 .stat-yellow { border-color: #fde68a; background: #fffbeb; }
 .stat-yellow { border-color: #fde68a; background: #fffbeb; }
 .stat-blue { border-color: #bfdbfe; background: #eff6ff; }
 .stat-blue { border-color: #bfdbfe; background: #eff6ff; }
@@ -1759,6 +1776,7 @@ onUnmounted(() => {
 .stat-orange { border-color: #fed7aa; background: #fff7ed; }
 .stat-orange { border-color: #fed7aa; background: #fff7ed; }
 .stat-indigo { border-color: #c7d2fe; background: #eef2ff; }
 .stat-indigo { border-color: #c7d2fe; background: #eef2ff; }
 .stat-slate { border-color: #e2e8f0; background: #f8fafc; }
 .stat-slate { border-color: #e2e8f0; background: #f8fafc; }
+.stat-purple { border-color: #e9d5ff; background: #faf5ff; }
 
 
 .text-yellow-700 { color: #a16207; }
 .text-yellow-700 { color: #a16207; }
 .text-blue-700 { color: #1d4ed8; }
 .text-blue-700 { color: #1d4ed8; }
@@ -1766,6 +1784,7 @@ onUnmounted(() => {
 .text-orange-700 { color: #c2410c; }
 .text-orange-700 { color: #c2410c; }
 .text-indigo-700 { color: #4338ca; }
 .text-indigo-700 { color: #4338ca; }
 .text-slate-800 { color: #1e293b; }
 .text-slate-800 { color: #1e293b; }
+.text-purple-700 { color: #6d28d9; }
 /* 主题选择器样式 */
 /* 主题选择器样式 */
 .theme-selector {
 .theme-selector {
   padding: 1rem 0;
   padding: 1rem 0;

+ 95 - 12
src/views/DomainManagementView.vue

@@ -1,12 +1,13 @@
 <script setup>
 <script setup>
-import { ref, onMounted, computed, inject } from 'vue'
+import { ref, onMounted, computed, inject, watch } from 'vue'
 import {
 import {
   listDomainManagement,
   listDomainManagement,
   createDomainManagement,
   createDomainManagement,
   updateDomainManagement,
   updateDomainManagement,
   deleteDomainManagement,
   deleteDomainManagement,
   getDomainManagement,
   getDomainManagement,
-  getDomainManagementTypes
+  getDomainManagementTypes,
+  getConfigDomains
 } from '@/services/api'
 } from '@/services/api'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
 import { useConfirm } from 'primevue/useconfirm'
 import { useConfirm } from 'primevue/useconfirm'
@@ -84,6 +85,47 @@ const teamOptions = computed(() => {
   }))
   }))
 })
 })
 
 
+// 域名配置列表
+const domainRedirectOptions = ref([])
+const loadingDomains = ref(false)
+const selectedDomainFromList = ref(null) // 用于存储从列表中选择的域名
+
+// 获取域名配置列表
+const fetchDomainOptions = async (teamId = null) => {
+  loadingDomains.value = true
+  try {
+    const result = await getConfigDomains(teamId)
+    domainRedirectOptions.value = (result.domainRedirect || []).map((domain) => ({
+      label: domain,
+      value: domain
+    }))
+  } catch (error) {
+    console.error('获取域名列表失败', error)
+    domainRedirectOptions.value = []
+  } finally {
+    loadingDomains.value = false
+  }
+}
+
+// 选择域名后填充到输入框
+const handleDomainSelect = () => {
+  if (selectedDomainFromList.value) {
+    domainForm.value.domain = selectedDomainFromList.value
+    // 清空选择,以便可以再次选择
+    selectedDomainFromList.value = null
+  }
+}
+
+// 监听团队ID变化,自动刷新域名列表
+watch(
+  () => domainForm.value.teamId,
+  async (newTeamId) => {
+    if (dialogVisible.value) {
+      await fetchDomainOptions(newTeamId || 0)
+    }
+  }
+)
+
 // 表单验证规则
 // 表单验证规则
 const formResolver = computed(() => {
 const formResolver = computed(() => {
   return zodResolver(
   return zodResolver(
@@ -122,13 +164,27 @@ const fetchData = async (page = 0) => {
       params.domain,
       params.domain,
       params.enabled
       params.enabled
     )
     )
-    tableData.value = result || {
-      data: [],
-      meta: {
-        page: 0,
-        size: 20,
-        total: 0,
-        totalPages: 0
+    if (result) {
+      // 确保 meta 中的数值类型正确
+      tableData.value = {
+        ...result,
+        meta: {
+          ...result.meta,
+          page: Number(result.meta?.page) || 0,
+          size: Number(result.meta?.size) || 20,
+          total: Number(result.meta?.total) || 0,
+          totalPages: Number(result.meta?.totalPages) || 0
+        }
+      }
+    } else {
+      tableData.value = {
+        data: [],
+        meta: {
+          page: 0,
+          size: 20,
+          total: 0,
+          totalPages: 0
+        }
       }
       }
     }
     }
   } catch (error) {
   } catch (error) {
@@ -154,7 +210,7 @@ const fetchData = async (page = 0) => {
 // 分页变化
 // 分页变化
 const handlePageChange = (event) => {
 const handlePageChange = (event) => {
   tableData.value.meta.page = event.page
   tableData.value.meta.page = event.page
-  tableData.value.meta.size = event.rows
+  tableData.value.meta.size = Number(event.rows) || 20
   fetchData(event.page)
   fetchData(event.page)
 }
 }
 
 
@@ -190,7 +246,7 @@ const getTeamName = (teamId) => {
 }
 }
 
 
 // 打开新建对话框
 // 打开新建对话框
-const openNewDialog = () => {
+const openNewDialog = async () => {
   domainForm.value = {
   domainForm.value = {
     id: null,
     id: null,
     teamId: null,
     teamId: null,
@@ -201,6 +257,8 @@ const openNewDialog = () => {
   }
   }
   isEditMode.value = false
   isEditMode.value = false
   dialogVisible.value = true
   dialogVisible.value = true
+  // 加载域名列表(全局配置,teamId=0)
+  await fetchDomainOptions(0)
 }
 }
 
 
 // 打开编辑对话框
 // 打开编辑对话框
@@ -217,6 +275,8 @@ const openEditDialog = async (domain) => {
     }
     }
     isEditMode.value = true
     isEditMode.value = true
     dialogVisible.value = true
     dialogVisible.value = true
+    // 加载域名列表(根据团队ID,如果没有团队ID则使用全局配置)
+    await fetchDomainOptions(detail.teamId || 0)
   } catch (error) {
   } catch (error) {
     toast.add({
     toast.add({
       severity: 'error',
       severity: 'error',
@@ -335,7 +395,7 @@ onMounted(() => {
         :paginator="true"
         :paginator="true"
         paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
         paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
         currentPageReportTemplate="{totalRecords} 条记录 "
         currentPageReportTemplate="{totalRecords} 条记录 "
-        :rows="tableData.meta.size"
+        :rows="Number(tableData.meta.size) || 20"
         :rowsPerPageOptions="[10, 20, 50, 100]"
         :rowsPerPageOptions="[10, 20, 50, 100]"
         :totalRecords="tableData.meta.total"
         :totalRecords="tableData.meta.total"
         @page="handlePageChange"
         @page="handlePageChange"
@@ -533,6 +593,29 @@ onMounted(() => {
             <Message v-if="$form.domain?.invalid" severity="error" size="small" variant="simple">
             <Message v-if="$form.domain?.invalid" severity="error" size="small" variant="simple">
               {{ $form.domain.error?.message }}
               {{ $form.domain.error?.message }}
             </Message>
             </Message>
+            <div class="mt-2 flex gap-2 items-center">
+              <Select
+                v-model="selectedDomainFromList"
+                :options="domainRedirectOptions"
+                optionLabel="label"
+                optionValue="value"
+                placeholder="从列表中选择域名"
+                :loading="loadingDomains"
+                :disabled="loadingDomains"
+                class="flex-1"
+                @change="handleDomainSelect"
+              />
+              <Button
+                icon="pi pi-refresh"
+                @click="fetchDomainOptions(domainForm.teamId || 0)"
+                size="small"
+                :loading="loadingDomains"
+                severity="secondary"
+                text
+                :title="'刷新域名列表'"
+              />
+            </div>
+            <small class="text-gray-500 mt-1">可以手动输入域名,也可以从列表中选择</small>
           </div>
           </div>
 
 
           <div class="field mt-4">
           <div class="field mt-4">

+ 68 - 1
src/views/DomainView.vue

@@ -9,7 +9,8 @@ import {
   showTeamDomains,
   showTeamDomains,
   getTeamDomainDailyStatistics,
   getTeamDomainDailyStatistics,
   getTeamDomainAllStatistics,
   getTeamDomainAllStatistics,
-  listMembers
+  listMembers,
+  getConfigDomains
 } from '@/services/api'
 } from '@/services/api'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
 import { useConfirm } from 'primevue/useconfirm'
 import { useConfirm } from 'primevue/useconfirm'
@@ -306,12 +307,17 @@ const onEdit = async (domain = null) => {
       if (isAdmin.value && detail.teamId) {
       if (isAdmin.value && detail.teamId) {
         await fetchTeamMembers(detail.teamId)
         await fetchTeamMembers(detail.teamId)
       }
       }
+      
+      // 加载域名列表(根据团队ID)
+      await fetchDomainOptions(detail.teamId || currentTeamId.value || 0)
     } catch (error) {
     } catch (error) {
       toast.add({ severity: 'error', summary: '错误', detail: '获取域名详情失败', life: 3000 })
       toast.add({ severity: 'error', summary: '错误', detail: '获取域名详情失败', life: 3000 })
       return
       return
     }
     }
   } else {
   } else {
     isEditing.value = false
     isEditing.value = false
+    // 加载域名列表(根据当前团队ID或全局配置)
+    await fetchDomainOptions(currentTeamId.value || 0)
   }
   }
 
 
   dialogVisible.value = true
   dialogVisible.value = true
@@ -465,6 +471,41 @@ const teamOptions = computed(() => {
 // 团队成员数据
 // 团队成员数据
 const teamMembers = ref([])
 const teamMembers = ref([])
 
 
+// 域名配置列表(domainRedirect)
+const domainRedirectOptions = ref([])
+const loadingDomains = ref(false)
+
+// 获取域名配置列表
+const fetchDomainOptions = async (teamId = null) => {
+  loadingDomains.value = true
+  try {
+    const result = await getConfigDomains(teamId)
+    domainRedirectOptions.value = (result.domainRedirect || []).map((domain) => ({
+      label: domain,
+      value: domain
+    }))
+  } catch (error) {
+    console.error('获取域名列表失败', error)
+    domainRedirectOptions.value = []
+  } finally {
+    loadingDomains.value = false
+  }
+}
+
+// 选择域名后填充到输入框
+const handleDomainSelect = (event) => {
+  // PrimeVue Select 的 change 事件传递的是事件对象,需要从 event.value 获取值
+  const selectedDomain = event?.value || event
+  if (selectedDomain && typeof selectedDomain === 'string') {
+    // 如果当前输入框有内容,追加到新行;否则直接设置
+    if (domainModel.domain && domainModel.domain.trim()) {
+      domainModel.domain = domainModel.domain + '\n' + selectedDomain
+    } else {
+      domainModel.domain = selectedDomain
+    }
+  }
+}
+
 // 计算团队成员选项
 // 计算团队成员选项
 const teamMemberOptions = computed(() => {
 const teamMemberOptions = computed(() => {
   if (!domainModel.teamId) return []
   if (!domainModel.teamId) return []
@@ -646,6 +687,9 @@ const handleTeamChange = async (event) => {
   if (!isPromoter.value) {
   if (!isPromoter.value) {
     await fetchTeamMembers(teamId)
     await fetchTeamMembers(teamId)
   }
   }
+  
+  // 刷新域名列表
+  await fetchDomainOptions(teamId || currentTeamId.value || 0)
 }
 }
 
 
 // 获取团队名称
 // 获取团队名称
@@ -957,7 +1001,30 @@ onMounted(() => {
           <div class="flex flex-col gap-2">
           <div class="flex flex-col gap-2">
             <label class="font-medium">域名</label>
             <label class="font-medium">域名</label>
             <Textarea v-model="domainModel.domain" placeholder="请输入域名,多个域名请换行输入" rows="4" />
             <Textarea v-model="domainModel.domain" placeholder="请输入域名,多个域名请换行输入" rows="4" />
+            <div class="mt-2 flex gap-2 items-center">
+              <Select
+                :modelValue="null"
+                :options="domainRedirectOptions"
+                optionLabel="label"
+                optionValue="value"
+                placeholder="从列表中选择域名"
+                :loading="loadingDomains"
+                :disabled="loadingDomains"
+                class="flex-1"
+                @change="handleDomainSelect"
+              />
+              <Button
+                icon="pi pi-refresh"
+                @click="fetchDomainOptions(domainModel.teamId || currentTeamId || 0)"
+                size="small"
+                :loading="loadingDomains"
+                severity="secondary"
+                text
+                :title="'刷新域名列表'"
+              />
+            </div>
             <small v-if="!domainModel.domain" class="text-red-500">请输入域名</small>
             <small v-if="!domainModel.domain" class="text-red-500">请输入域名</small>
+            <small v-else class="text-gray-500">可以手动输入域名,也可以从列表中选择(选择后会追加到输入框)</small>
           </div>
           </div>
 
 
           <div class="flex flex-col gap-2">
           <div class="flex flex-col gap-2">

+ 290 - 3
src/views/FinanceView.vue

@@ -84,6 +84,7 @@
           <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
           <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
           <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
           <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
           <div class="flex-1"></div>
           <div class="flex-1"></div>
+          <Button v-if="!isAdmin" icon="pi pi-plus" @click="openCreateDialog" label="新增提现申请" size="small" severity="success" />
         </div>
         </div>
       </template>
       </template>
 
 
@@ -216,7 +217,7 @@
         <template #body="slotProps">
         <template #body="slotProps">
           <div class="flex justify-center gap-1">
           <div class="flex justify-center gap-1">
             <Button
             <Button
-              v-if="slotProps.data.status === 'processing'"
+              v-if="isAdmin && slotProps.data.status === 'processing'"
               icon="pi pi-check"
               icon="pi pi-check"
               severity="success"
               severity="success"
               size="small"
               size="small"
@@ -226,7 +227,7 @@
               title="通过"
               title="通过"
             />
             />
             <Button
             <Button
-              v-if="slotProps.data.status === 'processing'"
+              v-if="isAdmin && slotProps.data.status === 'processing'"
               icon="pi pi-times"
               icon="pi pi-times"
               severity="danger"
               severity="danger"
               size="small"
               size="small"
@@ -258,6 +259,113 @@
       </Column>
       </Column>
     </DataTable>
     </DataTable>
 
 
+    <!-- 创建弹窗 -->
+    <Dialog
+      v-model:visible="createDialog"
+      :modal="true"
+      header="新增提现申请"
+      :style="{ width: '600px' }"
+      position="center"
+    >
+      <div class="p-fluid">
+        <div class="field">
+          <label for="create-paymentName" class="font-medium text-sm mb-2 block">收款名称 <span class="text-red-500">*</span></label>
+          <InputText id="create-paymentName" v-model="createForm.paymentName" class="w-full" placeholder="请输入收款名称" />
+        </div>
+
+        <div class="field mt-4">
+          <label for="create-reminderAmount" class="font-medium text-sm mb-2 block">提现金额 <span class="text-red-500">*</span></label>
+          <InputNumber
+            id="create-reminderAmount"
+            v-model="createForm.reminderAmount"
+            mode="currency"
+            currency="CNY"
+            locale="zh-CN"
+            class="w-full"
+            placeholder="请输入提现金额"
+            :min="100"
+          />
+          <small class="text-gray-500 mt-1 block">最低提现金额为100元</small>
+        </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="!createQrCodePreview"
+              mode="basic"
+              @select="onCreateQrCodeFileSelect"
+              customUpload
+              auto
+              severity="secondary"
+              class="p-button-outlined"
+              accept="image/*"
+              :maxFileSize="50000000"
+              chooseLabel="选择收款码"
+            />
+            <div v-if="createQrCodePreview" class="flex flex-col items-center gap-2">
+              <img
+                :src="createQrCodePreview"
+                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="removeCreateQrCode"
+                title="移除收款码"
+              />
+            </div>
+          </div>
+        </div>
+
+        <div class="field mt-4">
+          <label for="create-paymentAccount" class="font-medium text-sm mb-2 block">收款账户 <span class="text-red-500">*</span></label>
+          <InputText id="create-paymentAccount" v-model="createForm.paymentAccount" class="w-full" placeholder="请输入收款账户" />
+        </div>
+
+        <div class="field mt-4">
+          <label for="create-bankName" class="font-medium text-sm mb-2 block">开户行 <span class="text-red-500">*</span></label>
+          <InputText id="create-bankName" v-model="createForm.bankName" class="w-full" placeholder="请输入开户行" />
+        </div>
+
+        <div class="field mt-4">
+          <label for="create-withdrawPassword" class="font-medium text-sm mb-2 block">提现密码 <span class="text-red-500">*</span></label>
+          <Password
+            id="create-withdrawPassword"
+            v-model="createForm.withdrawPassword"
+            class="w-full"
+            placeholder="请输入提现密码"
+            toggleMask
+            :feedback="false"
+            autocomplete="off"
+          />
+        </div>
+
+        <div class="field mt-4">
+          <label for="create-remark" class="font-medium text-sm mb-2 block">备注</label>
+          <Textarea
+            id="create-remark"
+            v-model="createForm.remark"
+            rows="3"
+            class="w-full"
+            placeholder="请输入备注信息..."
+            autoResize
+          />
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="取消" severity="secondary" @click="createDialog = false" />
+          <Button label="提交申请" severity="success" @click="saveCreate" :loading="createLoading" />
+        </div>
+      </template>
+    </Dialog>
+
     <!-- 编辑弹窗 -->
     <!-- 编辑弹窗 -->
     <Dialog
     <Dialog
       v-model:visible="editDialog"
       v-model:visible="editDialog"
@@ -401,9 +509,10 @@ import InputText from 'primevue/inputtext'
 import InputNumber from 'primevue/inputnumber'
 import InputNumber from 'primevue/inputnumber'
 import Textarea from 'primevue/textarea'
 import Textarea from 'primevue/textarea'
 import FileUpload from 'primevue/fileupload'
 import FileUpload from 'primevue/fileupload'
+import Password from 'primevue/password'
 import { useConfirm } from 'primevue/useconfirm'
 import { useConfirm } from 'primevue/useconfirm'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
-import { listFinance, updateFinance, deleteFinance, updateFinanceStatus, uploadImage } from '@/services/api'
+import { listFinance, createFinance, updateFinance, deleteFinance, updateFinanceStatus, uploadImage } from '@/services/api'
 import { FinanceStatus } from '@/enums'
 import { FinanceStatus } from '@/enums'
 import { useTeamStore } from '@/stores/team'
 import { useTeamStore } from '@/stores/team'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
@@ -428,6 +537,23 @@ const tableData = ref({
 // 加载状态
 // 加载状态
 const loading = ref(false)
 const loading = ref(false)
 
 
+// 创建相关
+const createDialog = ref(false)
+const createLoading = ref(false)
+const createForm = ref({
+  paymentName: null,
+  reminderAmount: null,
+  paymentAccount: null,
+  bankName: null,
+  remark: null,
+  paymentQrCode: null,
+  withdrawPassword: null
+})
+
+// 创建时的图片上传相关
+const createQrCodePreview = ref(null)
+const createQrCodeFile = ref(null)
+
 // 编辑相关
 // 编辑相关
 const editDialog = ref(false)
 const editDialog = ref(false)
 const editLoading = ref(false)
 const editLoading = ref(false)
@@ -863,6 +989,167 @@ const previewQrCode = (qrCodeData) => {
   }
   }
 }
 }
 
 
+// 打开创建弹窗
+const openCreateDialog = () => {
+  createForm.value = {
+    paymentName: null,
+    reminderAmount: null,
+    paymentAccount: null,
+    bankName: null,
+    remark: null,
+    paymentQrCode: null,
+    withdrawPassword: null
+  }
+  createQrCodePreview.value = null
+  createQrCodeFile.value = null
+  createDialog.value = true
+}
+
+// 创建时的文件选择处理
+const onCreateQrCodeFileSelect = (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) => {
+    createQrCodePreview.value = e.target.result
+    createQrCodeFile.value = file
+  }
+  reader.readAsDataURL(file)
+}
+
+// 移除创建时的收款码
+const removeCreateQrCode = () => {
+  createQrCodePreview.value = null
+  createQrCodeFile.value = null
+  createForm.value.paymentQrCode = null
+}
+
+// 保存创建
+const saveCreate = async () => {
+  // 验证必填字段
+  if (!createForm.value.paymentName || !createForm.value.paymentName.trim()) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '请输入收款名称',
+      life: 3000
+    })
+    return
+  }
+
+  if (!createForm.value.reminderAmount || createForm.value.reminderAmount < 100) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '提现金额不能低于100元',
+      life: 3000
+    })
+    return
+  }
+
+  if (!createForm.value.paymentAccount || !createForm.value.paymentAccount.trim()) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '请输入收款账户',
+      life: 3000
+    })
+    return
+  }
+
+  if (!createForm.value.bankName || !createForm.value.bankName.trim()) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '请输入开户行',
+      life: 3000
+    })
+    return
+  }
+
+  if (!createForm.value.withdrawPassword || !createForm.value.withdrawPassword.trim()) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '请输入提现密码',
+      life: 3000
+    })
+    return
+  }
+
+  createLoading.value = true
+  try {
+    // 准备保存的数据
+    const saveData = { ...createForm.value }
+
+    // 处理图片逻辑
+    if (createQrCodeFile.value) {
+      // 有新选择的图片文件,先上传
+      try {
+        const result = await uploadImage(createQrCodeFile.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
+      }
+    }
+
+    await createFinance(saveData)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '提现申请提交成功',
+      life: 3000
+    })
+    createDialog.value = false
+    fetchData()
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '提交失败: ' + (error.message || '未知错误'),
+      life: 3000
+    })
+  } finally {
+    createLoading.value = false
+  }
+}
+
 // 初始化
 // 初始化
 onMounted(async () => {
 onMounted(async () => {
   // 根据角色加载相应的数据
   // 根据角色加载相应的数据

+ 68 - 2
src/views/LandingDomainPoolView.vue

@@ -7,7 +7,8 @@ import {
   deleteLandingDomainPool,
   deleteLandingDomainPool,
   getLandingDomainPool,
   getLandingDomainPool,
   showLandingDomainPools,
   showLandingDomainPools,
-  listMembers
+  listMembers,
+  getConfigDomains
 } from '@/services/api'
 } from '@/services/api'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
 import { useConfirm } from 'primevue/useconfirm'
 import { useConfirm } from 'primevue/useconfirm'
@@ -233,6 +234,9 @@ const onEdit = async (pool = null) => {
       if (isAdmin.value && detail.teamId) {
       if (isAdmin.value && detail.teamId) {
         await fetchTeamMembers(detail.teamId)
         await fetchTeamMembers(detail.teamId)
       }
       }
+      
+      // 加载域名列表(根据团队ID)
+      await fetchDomainOptions(detail.teamId || currentTeamId.value || 0)
     } catch (error) {
     } catch (error) {
       toast.add({ severity: 'error', summary: '错误', detail: '获取落地域名池详情失败', life: 3000 })
       toast.add({ severity: 'error', summary: '错误', detail: '获取落地域名池详情失败', life: 3000 })
       return
       return
@@ -243,6 +247,8 @@ const onEdit = async (pool = null) => {
     if (!isAdmin.value && currentTeamId.value) {
     if (!isAdmin.value && currentTeamId.value) {
       await fetchTeamMembersForCurrentUser()
       await fetchTeamMembersForCurrentUser()
     }
     }
+    // 加载域名列表(根据当前团队ID或全局配置)
+    await fetchDomainOptions(currentTeamId.value || 0)
   }
   }
 
 
   dialogVisible.value = true
   dialogVisible.value = true
@@ -408,6 +414,41 @@ const getTeamName = (teamId) => {
 // 团队成员数据
 // 团队成员数据
 const teamMembers = ref([])
 const teamMembers = ref([])
 
 
+// 域名配置列表(domainLand)
+const domainLandOptions = ref([])
+const loadingDomains = ref(false)
+
+// 获取域名配置列表
+const fetchDomainOptions = async (teamId = null) => {
+  loadingDomains.value = true
+  try {
+    const result = await getConfigDomains(teamId)
+    domainLandOptions.value = (result.domainLand || []).map((domain) => ({
+      label: domain,
+      value: domain
+    }))
+  } catch (error) {
+    console.error('获取域名列表失败', error)
+    domainLandOptions.value = []
+  } finally {
+    loadingDomains.value = false
+  }
+}
+
+// 选择域名后填充到输入框
+const handleDomainSelect = (event) => {
+  // PrimeVue Select 的 change 事件传递的是事件对象,需要从 event.value 获取值
+  const selectedDomain = event?.value || event
+  if (selectedDomain && typeof selectedDomain === 'string') {
+    // 如果当前输入框有内容,追加分隔符和域名;否则直接设置
+    if (poolModel.domain && poolModel.domain.trim()) {
+      poolModel.domain = poolModel.domain + ', ' + selectedDomain
+    } else {
+      poolModel.domain = selectedDomain
+    }
+  }
+}
+
 // 计算团队成员选项
 // 计算团队成员选项
 const teamMemberOptions = computed(() => {
 const teamMemberOptions = computed(() => {
   if (!poolModel.teamId) return []
   if (!poolModel.teamId) return []
@@ -618,6 +659,9 @@ const handleTeamChange = async (event) => {
   } else {
   } else {
     teamMembers.value = []
     teamMembers.value = []
   }
   }
+  
+  // 刷新域名列表
+  await fetchDomainOptions(teamId || currentTeamId.value || 0)
 }
 }
 
 
 // 获取绑定用户名
 // 获取绑定用户名
@@ -891,9 +935,31 @@ onMounted(() => {
               :placeholder="isEditing ? '请输入域名' : '请输入域名,多个域名可用逗号、分号或换行分隔'"
               :placeholder="isEditing ? '请输入域名' : '请输入域名,多个域名可用逗号、分号或换行分隔'"
               rows="4"
               rows="4"
             />
             />
+            <div class="mt-2 flex gap-2 items-center">
+              <Select
+                :modelValue="null"
+                :options="domainLandOptions"
+                optionLabel="label"
+                optionValue="value"
+                placeholder="从列表中选择域名"
+                :loading="loadingDomains"
+                :disabled="loadingDomains"
+                class="flex-1"
+                @change="handleDomainSelect"
+              />
+              <Button
+                icon="pi pi-refresh"
+                @click="fetchDomainOptions(poolModel.teamId || currentTeamId || 0)"
+                size="small"
+                :loading="loadingDomains"
+                severity="secondary"
+                text
+                :title="'刷新域名列表'"
+              />
+            </div>
             <small v-if="!poolModel.domain" class="text-red-500">请输入域名</small>
             <small v-if="!poolModel.domain" class="text-red-500">请输入域名</small>
             <small v-else class="text-gray-500">
             <small v-else class="text-gray-500">
-              {{ isEditing ? '编辑时只能修改单个域名' : '支持分隔符:,(逗号)、;(分号)、换行' }}
+              {{ isEditing ? '编辑时只能修改单个域名' : '可以手动输入域名,也可以从列表中选择(选择后会追加到输入框)。支持分隔符:,(逗号)、;(分号)、换行' }}
             </small>
             </small>
           </div>
           </div>
 
 

+ 335 - 4
src/views/LinkView.vue

@@ -34,6 +34,14 @@
         size="small"
         size="small"
         severity="success"
         severity="success"
       />
       />
+      <Button
+        v-if="isAdmin"
+        icon="pi pi-building"
+        @click="openGenerateTeamLinkDialog"
+        label="生成团队链接"
+        size="small"
+        severity="info"
+      />
       <Button
       <Button
         v-if="isAdmin || isTeam"
         v-if="isAdmin || isTeam"
         icon="pi pi-plus"
         icon="pi pi-plus"
@@ -61,10 +69,18 @@
               <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
               <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
             </div>
             </div>
 
 
-            <!-- 图片 -->
+            <!-- 图片或二维码 -->
             <div class="px-4 py-2">
             <div class="px-4 py-2">
               <div class="image-container-card">
               <div class="image-container-card">
                 <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
                 <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
+                <img 
+                  v-else-if="getLinkQRCode(link.id)" 
+                  :src="getLinkQRCode(link.id)" 
+                  :alt="link.name + '二维码'" 
+                  class="link-image-card qr-code-clickable" 
+                  @click="openQRCodeDialog(link)"
+                  title="点击查看大图并保存"
+                />
                 <div v-else class="no-image-placeholder">
                 <div v-else class="no-image-placeholder">
                   <i class="pi pi-image text-gray-400"></i>
                   <i class="pi pi-image text-gray-400"></i>
                 </div>
                 </div>
@@ -127,10 +143,18 @@
               <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
               <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
             </div>
             </div>
 
 
-            <!-- 图片 -->
+            <!-- 图片或二维码 -->
             <div class="px-4 py-2">
             <div class="px-4 py-2">
               <div class="image-container-card">
               <div class="image-container-card">
                 <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
                 <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
+                <img 
+                  v-else-if="getLinkQRCode(link.id)" 
+                  :src="getLinkQRCode(link.id)" 
+                  :alt="link.name + '二维码'" 
+                  class="link-image-card qr-code-clickable" 
+                  @click="openQRCodeDialog(link)"
+                  title="点击查看大图并保存"
+                />
                 <div v-else class="no-image-placeholder">
                 <div v-else class="no-image-placeholder">
                   <i class="pi pi-image text-gray-400"></i>
                   <i class="pi pi-image text-gray-400"></i>
                 </div>
                 </div>
@@ -193,10 +217,18 @@
               <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
               <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
             </div>
             </div>
 
 
-            <!-- 图片 -->
+            <!-- 图片或二维码 -->
             <div class="px-4 py-2">
             <div class="px-4 py-2">
               <div class="image-container-card">
               <div class="image-container-card">
                 <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
                 <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
+                <img 
+                  v-else-if="getLinkQRCode(link.id)" 
+                  :src="getLinkQRCode(link.id)" 
+                  :alt="link.name + '二维码'" 
+                  class="link-image-card qr-code-clickable" 
+                  @click="openQRCodeDialog(link)"
+                  title="点击查看大图并保存"
+                />
                 <div v-else class="no-image-placeholder">
                 <div v-else class="no-image-placeholder">
                   <i class="pi pi-image text-gray-400"></i>
                   <i class="pi pi-image text-gray-400"></i>
                 </div>
                 </div>
@@ -422,6 +454,105 @@
         </div>
         </div>
       </template>
       </template>
     </Dialog>
     </Dialog>
+
+    <!-- 生成团队链接弹窗 -->
+    <Dialog
+      v-model:visible="generateTeamLinkDialog"
+      :modal="true"
+      header="生成团队链接"
+      :style="{ width: '600px' }"
+      position="center"
+    >
+      <div class="p-fluid">
+        <div class="field">
+          <label for="generate-team-link-teamId" class="font-medium text-sm mb-2 block">选择团队</label>
+          <Select
+            id="generate-team-link-teamId"
+            v-model="generateTeamLinkForm.teamId"
+            :options="teamSelectOptions"
+            optionLabel="label"
+            optionValue="value"
+            placeholder="选择团队"
+            class="w-full"
+            showClear
+          />
+        </div>
+
+        <div v-if="generateTeamLinkData.generalLink" class="field mt-4">
+          <label class="font-medium text-sm mb-2 block">通用链接</label>
+          <div class="flex items-center gap-2">
+            <InputText :value="generateTeamLinkData.generalLink" readonly class="flex-1" />
+            <Button
+              icon="pi pi-copy"
+              size="small"
+              @click="copyToClipboard(generateTeamLinkData.generalLink)"
+              title="复制通用链接"
+            />
+          </div>
+        </div>
+
+        <div v-if="generateTeamLinkData.browserLink" class="field mt-4">
+          <label class="font-medium text-sm mb-2 block">浏览器链接</label>
+          <div class="flex items-center gap-2">
+            <InputText :value="generateTeamLinkData.browserLink" readonly class="flex-1" />
+            <Button
+              icon="pi pi-copy"
+              size="small"
+              @click="copyToClipboard(generateTeamLinkData.browserLink)"
+              title="复制浏览器链接"
+            />
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="关闭" severity="secondary" @click="generateTeamLinkDialog = false" />
+          <Button
+            label="生成链接"
+            severity="success"
+            @click="handleGenerateTeamLink"
+            :loading="generateTeamLinkLoading"
+            :disabled="!generateTeamLinkForm.teamId"
+          />
+        </div>
+      </template>
+    </Dialog>
+
+    <!-- 二维码预览弹窗 -->
+    <Dialog
+      v-model:visible="qrCodeDialog"
+      :modal="true"
+      :header="qrCodeLinkName + ' - 二维码'"
+      :style="{ width: '500px' }"
+      position="center"
+    >
+      <div class="flex flex-col items-center gap-4">
+        <div v-if="qrCodeImage" class="qr-code-preview-container">
+          <img :src="qrCodeImage" alt="二维码" class="qr-code-preview-image" />
+        </div>
+        <p v-else class="text-gray-500">暂无二维码图片</p>
+        <div v-if="qrCodeLinkUrl" class="w-full">
+          <label class="font-medium text-sm mb-2 block text-gray-700">链接地址</label>
+          <div class="flex items-center gap-2">
+            <InputText :value="qrCodeLinkUrl" readonly class="flex-1 text-sm" />
+            <Button
+              icon="pi pi-copy"
+              size="small"
+              @click="copyToClipboard(qrCodeLinkUrl)"
+              title="复制链接"
+            />
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="保存二维码" icon="pi pi-download" severity="success" @click="saveQRCode" />
+          <Button label="关闭" severity="secondary" @click="qrCodeDialog = false" />
+        </div>
+      </template>
+    </Dialog>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -435,10 +566,11 @@ import FileUpload from 'primevue/fileupload'
 import { useConfirm } from 'primevue/useconfirm'
 import { useConfirm } from 'primevue/useconfirm'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
 import { usePrimeVue } from 'primevue/config'
 import { usePrimeVue } from 'primevue/config'
-import { listLinks, createLink, updateLink, deleteLink, uploadImage, listMembers, getPromotionLink } from '@/services/api'
+import { listLinks, createLink, updateLink, deleteLink, uploadImage, listMembers, getPromotionLink, generateFirstLevelAgentLink } from '@/services/api'
 import { LinkType } from '@/enums'
 import { LinkType } from '@/enums'
 import { useTeamStore } from '@/stores/team'
 import { useTeamStore } from '@/stores/team'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
+import QRCode from 'qrcode'
 
 
 const toast = useToast()
 const toast = useToast()
 const confirm = useConfirm()
 const confirm = useConfirm()
@@ -462,6 +594,9 @@ const tableData = ref({
 // 过滤后的链接数据
 // 过滤后的链接数据
 const filteredLinks = ref([])
 const filteredLinks = ref([])
 
 
+// 二维码缓存
+const qrCodeCache = ref(new Map())
+
 // 加载状态
 // 加载状态
 const loading = ref(false)
 const loading = ref(false)
 
 
@@ -512,6 +647,17 @@ const generatePromoData = ref({
   promotionLink: null
   promotionLink: null
 })
 })
 
 
+// 生成团队链接相关
+const generateTeamLinkDialog = ref(false)
+const generateTeamLinkLoading = ref(false)
+const generateTeamLinkForm = ref({
+  teamId: null
+})
+const generateTeamLinkData = ref({
+  generalLink: null,
+  browserLink: null
+})
+
 // 搜索表单
 // 搜索表单
 const searchForm = ref({
 const searchForm = ref({
   type: null,
   type: null,
@@ -579,6 +725,8 @@ const fetchData = async () => {
     )
     )
     tableData.value = response
     tableData.value = response
     applyFilters()
     applyFilters()
+    // 为所有链接生成二维码
+    await generateQRCodesForLinks(response.content || [])
   } catch {
   } catch {
     toast.add({
     toast.add({
       severity: 'error',
       severity: 'error',
@@ -606,6 +754,8 @@ const applyFilters = () => {
   }
   }
 
 
   filteredLinks.value = links
   filteredLinks.value = links
+  // 为没有图片的链接生成二维码
+  generateQRCodesForLinks(links)
 }
 }
 
 
 // 按类型获取链接
 // 按类型获取链接
@@ -745,6 +895,108 @@ const handleImageError = (event) => {
   event.target.style.display = 'none'
   event.target.style.display = 'none'
 }
 }
 
 
+// 生成二维码
+const generateQRCode = async (linkUrl) => {
+  if (!linkUrl) return null
+  
+  // 检查缓存
+  if (qrCodeCache.value.has(linkUrl)) {
+    return qrCodeCache.value.get(linkUrl)
+  }
+  
+  try {
+    const qrCodeDataUrl = await QRCode.toDataURL(linkUrl, {
+      width: 200,
+      margin: 1,
+      color: {
+        dark: '#000000',
+        light: '#FFFFFF'
+      }
+    })
+    qrCodeCache.value.set(linkUrl, qrCodeDataUrl)
+    return qrCodeDataUrl
+  } catch (error) {
+    console.error('生成二维码失败:', error)
+    return null
+  }
+}
+
+// 二维码数据存储(按链接ID)- 使用对象以支持响应式
+const linkQRCodes = ref({})
+
+// 二维码预览相关
+const qrCodeDialog = ref(false)
+const qrCodeImage = ref('')
+const qrCodeLinkName = ref('')
+const qrCodeLinkUrl = ref('')
+
+// 为链接生成二维码(如果还没有)
+const ensureQRCode = async (link) => {
+  if (!link || !link.link || link.image) return
+  
+  const linkId = link.id
+  if (linkQRCodes.value[linkId]) return
+  
+  const qrCode = await generateQRCode(link.link)
+  if (qrCode) {
+    linkQRCodes.value[linkId] = qrCode
+  }
+}
+
+// 批量生成二维码
+const generateQRCodesForLinks = async (links) => {
+  const promises = links
+    .filter(link => link && link.link && !link.image)
+    .map(link => ensureQRCode(link))
+  await Promise.all(promises)
+}
+
+// 获取链接的二维码
+const getLinkQRCode = (linkId) => {
+  return linkQRCodes.value[linkId] || null
+}
+
+// 打开二维码预览弹窗
+const openQRCodeDialog = (link) => {
+  const qrCode = getLinkQRCode(link.id)
+  if (qrCode) {
+    qrCodeImage.value = qrCode
+    qrCodeLinkName.value = link.name || '推广链接'
+    qrCodeLinkUrl.value = link.link || ''
+    qrCodeDialog.value = true
+  }
+}
+
+// 保存二维码
+const saveQRCode = () => {
+  if (!qrCodeImage.value) return
+  
+  try {
+    // 创建一个临时的 a 标签来下载图片
+    const link = document.createElement('a')
+    link.href = qrCodeImage.value
+    link.download = `${qrCodeLinkName.value || '二维码'}_${Date.now()}.png`
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '二维码已保存',
+      life: 2000
+    })
+  } catch (error) {
+    console.error('保存二维码失败:', error)
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '保存二维码失败',
+      life: 3000
+    })
+  }
+}
+
 // 打开新增弹窗
 // 打开新增弹窗
 const openAddDialog = () => {
 const openAddDialog = () => {
   isEdit.value = false
   isEdit.value = false
@@ -1099,6 +1351,58 @@ const handleGenerateLink = async () => {
   }
   }
 }
 }
 
 
+// 打开生成团队链接弹窗
+const openGenerateTeamLinkDialog = () => {
+  generateTeamLinkForm.value = {
+    teamId: null
+  }
+  generateTeamLinkData.value = {
+    generalLink: null,
+    browserLink: null
+  }
+  generateTeamLinkDialog.value = true
+}
+
+// 生成团队链接
+const handleGenerateTeamLink = async () => {
+  if (!generateTeamLinkForm.value.teamId) {
+    toast.add({
+      severity: 'warn',
+      summary: '提示',
+      detail: '请选择团队',
+      life: 3000
+    })
+    return
+  }
+
+  generateTeamLinkLoading.value = true
+  try {
+    const response = await generateFirstLevelAgentLink(generateTeamLinkForm.value.teamId)
+    generateTeamLinkData.value = {
+      generalLink: response.generalLink || null,
+      browserLink: response.browserLink || null
+    }
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: response.message || '团队链接生成成功',
+      life: 3000
+    })
+    // 生成成功后刷新链接列表
+    fetchData()
+  } catch (error) {
+    const errorMessage = error?.message || error?.detail || '生成团队链接失败'
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: errorMessage,
+      life: 3000
+    })
+  } finally {
+    generateTeamLinkLoading.value = false
+  }
+}
+
 // 初始化
 // 初始化
 onMounted(async () => {
 onMounted(async () => {
   if (isAdmin.value) {
   if (isAdmin.value) {
@@ -1195,4 +1499,31 @@ onMounted(async () => {
   background-color: #d1d5db;
   background-color: #d1d5db;
   transform: scale(0.98);
   transform: scale(0.98);
 }
 }
+
+.qr-code-clickable {
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.qr-code-clickable:hover {
+  transform: scale(1.05);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.qr-code-preview-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+  background-color: #f9fafb;
+  border-radius: 8px;
+  border: 1px solid #e5e7eb;
+}
+
+.qr-code-preview-image {
+  max-width: 100%;
+  max-height: 400px;
+  width: auto;
+  height: auto;
+}
 </style>
 </style>

+ 225 - 12
src/views/MainView.vue

@@ -14,7 +14,13 @@ import IconField from 'primevue/iconfield'
 import InputIcon from 'primevue/inputicon'
 import InputIcon from 'primevue/inputicon'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
-import { resetPasswordApi } from '@/services/api'
+import { 
+  resetPasswordApi, 
+  checkTeamWithdrawPasswordStatus,
+  updateTeamWithdrawPassword,
+  checkTeamMemberWithdrawPasswordStatus,
+  updateTeamMemberWithdrawPassword
+} from '@/services/api'
 import { zodResolver } from '@primevue/forms/resolvers/zod'
 import { zodResolver } from '@primevue/forms/resolvers/zod'
 import { z } from 'zod'
 import { z } from 'zod'
 
 
@@ -119,6 +125,12 @@ const allNavItems = [
     icon: 'pi pi-fw pi-globe',
     icon: 'pi pi-fw pi-globe',
     name: 'domain-management',
     name: 'domain-management',
     roles: ['admin']
     roles: ['admin']
+  },
+  {
+    label: '视频反馈',
+    icon: 'pi pi-fw pi-video',
+    name: 'video-feedback',
+    roles: ['admin']
   }
   }
 ]
 ]
 
 
@@ -144,21 +156,37 @@ const navItems = computed(() => {
   return filterItemsByRole(allNavItems, userInfo.role)
   return filterItemsByRole(allNavItems, userInfo.role)
 })
 })
 
 
-const userMenuItems = [
-  {
-    label: '修改密码',
-    icon: 'pi pi-fw pi-lock',
-    command: () => resetPassword()
-  },
-  {
+const userMenuItems = computed(() => {
+  const items = [
+    {
+      label: '修改密码',
+      icon: 'pi pi-fw pi-lock',
+      command: () => resetPassword()
+    }
+  ]
+  
+  // 根据角色添加修改个人提现密码选项
+  const role = userStore.userInfo?.role
+  if (role === 'team' || role === 'admin' || role === 'promoter') {
+    items.push({
+      label: '修改个人提现密码',
+      icon: 'pi pi-fw pi-key',
+      command: () => resetWithdrawPassword()
+    })
+  }
+  
+  items.push({
     separator: true
     separator: true
-  },
-  {
+  })
+  
+  items.push({
     label: '退出登录',
     label: '退出登录',
     icon: 'pi pi-fw pi-sign-out',
     icon: 'pi pi-fw pi-sign-out',
     command: () => logout()
     command: () => logout()
-  }
-]
+  })
+  
+  return items
+})
 
 
 const logout = () => {
 const logout = () => {
   userStore.logout()
   userStore.logout()
@@ -181,6 +209,49 @@ const resetPassword = () => {
   }
   }
 }
 }
 
 
+const withdrawPasswordData = ref({
+  visible: false,
+  oldPassword: '',
+  newPassword: '',
+  confirmPassword: '',
+  loading: false,
+  hasPassword: false,
+  checking: false
+})
+
+const resetWithdrawPassword = async () => {
+  withdrawPasswordData.value = {
+    visible: true,
+    oldPassword: '',
+    newPassword: '',
+    confirmPassword: '',
+    loading: false,
+    hasPassword: false,
+    checking: true
+  }
+  
+  // 检查密码状态
+  try {
+    const role = userStore.userInfo?.role
+    let statusData
+    if (role === 'team' || role === 'admin') {
+      statusData = await checkTeamWithdrawPasswordStatus()
+    } else if (role === 'promoter') {
+      statusData = await checkTeamMemberWithdrawPasswordStatus()
+    }
+    withdrawPasswordData.value.hasPassword = statusData?.hasPassword || false
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '检查提现密码状态失败',
+      life: 2000
+    })
+  } finally {
+    withdrawPasswordData.value.checking = false
+  }
+}
+
 const resolver = zodResolver(
 const resolver = zodResolver(
   z.object({
   z.object({
     password: z.string().min(8, { message: '密码至少8位' }),
     password: z.string().min(8, { message: '密码至少8位' }),
@@ -216,6 +287,66 @@ const handleResetPassword = async ({ valid, values }) => {
     })
     })
   }
   }
 }
 }
+
+const withdrawPasswordResolver = computed(() => {
+  const schema = {
+    newPassword: z.string().min(1, { message: '新密码不能为空' }),
+    confirmPassword: z.string().refine((val) => val === withdrawPasswordData.value.newPassword, {
+      message: '密码不一致'
+    })
+  }
+  
+  // 如果已设置密码,需要验证旧密码
+  if (withdrawPasswordData.value.hasPassword) {
+    schema.oldPassword = z.string().min(1, { message: '请提供旧密码' })
+  }
+  
+  return zodResolver(z.object(schema))
+})
+
+const handleResetWithdrawPassword = async ({ valid, values }) => {
+  if (!valid) {
+    return
+  }
+  
+  withdrawPasswordData.value.loading = true
+  try {
+    const role = userStore.userInfo?.role
+    if (role === 'team' || role === 'admin') {
+      await updateTeamWithdrawPassword(
+        withdrawPasswordData.value.hasPassword ? values.oldPassword : null,
+        values.newPassword
+      )
+    } else if (role === 'promoter') {
+      await updateTeamMemberWithdrawPassword(
+        withdrawPasswordData.value.hasPassword ? values.oldPassword : null,
+        values.newPassword
+      )
+    }
+    
+    withdrawPasswordData.value.loading = false
+    withdrawPasswordData.value.visible = false
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '提现密码修改成功',
+      life: 2000
+    })
+    
+    // 重置表单
+    withdrawPasswordData.value.oldPassword = ''
+    withdrawPasswordData.value.newPassword = ''
+    withdrawPasswordData.value.confirmPassword = ''
+  } catch (error) {
+    withdrawPasswordData.value.loading = false
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '提现密码修改失败',
+      life: 2000
+    })
+  }
+}
 </script>
 </script>
 
 
 <template>
 <template>
@@ -305,6 +436,88 @@ const handleResetPassword = async ({ valid, values }) => {
         </div>
         </div>
       </Form>
       </Form>
     </Dialog>
     </Dialog>
+
+    <Dialog 
+      v-model:visible="withdrawPasswordData.visible" 
+      modal 
+      :header="withdrawPasswordData.hasPassword ? '修改提现密码' : '设置提现密码'" 
+      :style="{ width: '25rem' }"
+    >
+      <div v-if="withdrawPasswordData.checking" class="text-center py-4">
+        <i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
+        <p class="mt-2">检查密码状态中...</p>
+      </div>
+      <Form
+        v-else
+        v-slot="$form"
+        :resolver="withdrawPasswordResolver"
+        :initialValues="withdrawPasswordData"
+        @submit="handleResetWithdrawPassword"
+        class="p-fluid"
+      >
+        <FloatLabel v-if="withdrawPasswordData.hasPassword" variant="on" class="mt-2">
+          <IconField>
+            <InputIcon class="pi pi-lock" />
+            <Password
+              id="oldWithdrawPassword"
+              name="oldPassword"
+              v-model="withdrawPasswordData.oldPassword"
+              fluid
+              toggleMask
+              :feedback="false"
+              autocomplete="off"
+            />
+          </IconField>
+          <label for="oldWithdrawPassword">旧密码</label>
+        </FloatLabel>
+        <Message v-if="withdrawPasswordData.hasPassword && $form.oldPassword?.invalid" severity="error" size="small" variant="simple">{{
+          $form.oldPassword.error?.message
+        }}</Message>
+        
+        <FloatLabel variant="on" :class="withdrawPasswordData.hasPassword ? 'mt-4' : 'mt-2'">
+          <IconField>
+            <InputIcon class="pi pi-key" />
+            <Password
+              id="newWithdrawPassword"
+              name="newPassword"
+              v-model="withdrawPasswordData.newPassword"
+              fluid
+              toggleMask
+              :feedback="false"
+              autocomplete="off"
+            />
+          </IconField>
+          <label for="newWithdrawPassword">新密码</label>
+        </FloatLabel>
+        <Message v-if="$form.newPassword?.invalid" severity="error" size="small" variant="simple">{{
+          $form.newPassword.error?.message
+        }}</Message>
+        
+        <FloatLabel variant="on" class="mt-4">
+          <IconField>
+            <InputIcon class="pi pi-key" />
+            <Password
+              id="confirmWithdrawPassword"
+              name="confirmPassword"
+              v-model="withdrawPasswordData.confirmPassword"
+              fluid
+              toggleMask
+              :feedback="false"
+              autocomplete="off"
+            />
+          </IconField>
+          <label for="confirmWithdrawPassword">重复新密码</label>
+        </FloatLabel>
+        <Message v-if="$form.confirmPassword?.invalid" severity="error" size="small" variant="simple">{{
+          $form.confirmPassword.error?.message
+        }}</Message>
+        
+        <div class="field mt-4 text-right">
+          <Button label="取消" severity="secondary" @click="withdrawPasswordData.visible = false" />
+          <Button label="保存" type="submit" :loading="withdrawPasswordData.loading" class="ml-4" />
+        </div>
+      </Form>
+    </Dialog>
   </div>
   </div>
 </template>
 </template>
 
 

+ 201 - 9
src/views/TeamView.vue

@@ -119,7 +119,7 @@
 
 
       <Column
       <Column
         header="操作"
         header="操作"
-        style="min-width: 120px; width: 120px"
+        style="min-width: 200px; width: 200px"
         align-frozen="right"
         align-frozen="right"
         frozen
         frozen
         :pt="{
         :pt="{
@@ -130,6 +130,25 @@
       >
       >
         <template #body="slotProps">
         <template #body="slotProps">
           <div class="flex justify-center gap-1">
           <div class="flex justify-center gap-1">
+            <Button
+              icon="pi pi-link"
+              severity="success"
+              size="small"
+              text
+              rounded
+              aria-label="生成团队链接"
+              @click="handleGenerateTeamLink(slotProps.data)"
+              :loading="generatingLinkId === slotProps.data.id"
+            />
+            <Button
+              icon="pi pi-eye"
+              severity="info"
+              size="small"
+              text
+              rounded
+              aria-label="查看团队链接"
+              @click="handleViewTeamLink(slotProps.data)"
+            />
             <Button
             <Button
               icon="pi pi-pencil"
               icon="pi pi-pencil"
               severity="info"
               severity="info"
@@ -160,13 +179,30 @@
           <div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
           <div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
             <div class="flex items-center justify-between mb-4">
             <div class="flex items-center justify-between mb-4">
               <div class="text-xl font-extrabold text-gray-900">我的团队信息</div>
               <div class="text-xl font-extrabold text-gray-900">我的团队信息</div>
-              <Button
-                icon="pi pi-palette"
-                label="选择主题"
-                size="small"
-                severity="secondary"
-                @click="openThemeDialog"
-              />
+              <div class="flex gap-2">
+                <Button
+                  icon="pi pi-link"
+                  label="生成团队链接"
+                  size="small"
+                  severity="success"
+                  @click="handleGenerateTeamLink(teamData)"
+                  :loading="generatingLinkId === teamData.id"
+                />
+                <Button
+                  icon="pi pi-eye"
+                  label="查看团队链接"
+                  size="small"
+                  severity="info"
+                  @click="handleViewTeamLink(teamData)"
+                />
+                <Button
+                  icon="pi pi-palette"
+                  label="选择主题"
+                  size="small"
+                  severity="secondary"
+                  @click="openThemeDialog"
+                />
+              </div>
             </div>
             </div>
             <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
             <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
               <div class="flex flex-col">
               <div class="flex flex-col">
@@ -384,6 +420,54 @@
         </div>
         </div>
       </template>
       </template>
     </Dialog>
     </Dialog>
+
+    <!-- 团队链接弹窗 -->
+    <Dialog
+      v-model:visible="teamLinkDialog"
+      :modal="true"
+      header="团队推广链接"
+      :style="{ width: '600px' }"
+      position="center"
+    >
+      <div class="p-fluid">
+        <div v-if="teamLinkData.generalLink" class="field">
+          <label class="font-medium text-sm mb-2 block">通用链接</label>
+          <div class="flex items-center gap-2">
+            <InputText :value="teamLinkData.generalLink" readonly class="flex-1" />
+            <Button
+              icon="pi pi-copy"
+              size="small"
+              @click="copyToClipboard(teamLinkData.generalLink)"
+              title="复制通用链接"
+            />
+          </div>
+        </div>
+
+        <div v-if="teamLinkData.browserLink" class="field mt-4">
+          <label class="font-medium text-sm mb-2 block">浏览器链接</label>
+          <div class="flex items-center gap-2">
+            <InputText :value="teamLinkData.browserLink" readonly class="flex-1" />
+            <Button
+              icon="pi pi-copy"
+              size="small"
+              @click="copyToClipboard(teamLinkData.browserLink)"
+              title="复制浏览器链接"
+            />
+          </div>
+        </div>
+
+        <div v-if="!teamLinkData.generalLink && !teamLinkData.browserLink" class="text-center py-8">
+          <i class="pi pi-info-circle text-4xl text-gray-400 mb-4"></i>
+          <p class="text-gray-500">暂无团队链接,请先生成团队链接</p>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="关闭" severity="secondary" @click="teamLinkDialog = false" />
+        </div>
+      </template>
+    </Dialog>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -398,7 +482,7 @@ import InputNumber from 'primevue/inputnumber'
 import Password from 'primevue/password'
 import Password from 'primevue/password'
 import { useConfirm } from 'primevue/useconfirm'
 import { useConfirm } from 'primevue/useconfirm'
 import { useToast } from 'primevue/usetoast'
 import { useToast } from 'primevue/usetoast'
-import { listTeams, createTeam, updateTeam, deleteTeam, getTeamIpConversionRate, updateTeamThemeColor } from '@/services/api'
+import { listTeams, createTeam, updateTeam, deleteTeam, getTeamIpConversionRate, updateTeamThemeColor, generateFirstLevelAgentLink, listLinks } from '@/services/api'
 
 
 const toast = useToast()
 const toast = useToast()
 const confirm = useConfirm()
 const confirm = useConfirm()
@@ -431,6 +515,15 @@ const editForm = ref({
   confirmPassword: null
   confirmPassword: null
 })
 })
 
 
+// 团队链接相关
+const teamLinkDialog = ref(false)
+const teamLinkData = ref({
+  generalLink: null,
+  browserLink: null
+})
+const generatingLinkId = ref(null)
+const currentTeamIdForLink = ref(null)
+
 // 主题选择相关
 // 主题选择相关
 const themeDialog = ref(false)
 const themeDialog = ref(false)
 const themeLoading = ref(false)
 const themeLoading = ref(false)
@@ -680,6 +773,12 @@ const deleteTeamRecord = async (id) => {
 const copyToClipboard = async (text) => {
 const copyToClipboard = async (text) => {
   try {
   try {
     await navigator.clipboard.writeText(text)
     await navigator.clipboard.writeText(text)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '已复制到剪贴板',
+      life: 2000
+    })
   } catch {
   } catch {
     const textArea = document.createElement('textarea')
     const textArea = document.createElement('textarea')
     textArea.value = text
     textArea.value = text
@@ -687,6 +786,12 @@ const copyToClipboard = async (text) => {
     textArea.select()
     textArea.select()
     document.execCommand('copy')
     document.execCommand('copy')
     document.body.removeChild(textArea)
     document.body.removeChild(textArea)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '已复制到剪贴板',
+      life: 2000
+    })
   }
   }
 }
 }
 
 
@@ -827,6 +932,93 @@ const saveEdit = async () => {
   }
   }
 }
 }
 
 
+// 生成团队链接
+const handleGenerateTeamLink = async (team) => {
+  if (!team || !team.id) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '团队信息不存在',
+      life: 3000
+    })
+    return
+  }
+
+  generatingLinkId.value = team.id
+  try {
+    const response = await generateFirstLevelAgentLink(team.id)
+    teamLinkData.value = {
+      generalLink: response.generalLink || null,
+      browserLink: response.browserLink || null
+    }
+    currentTeamIdForLink.value = team.id
+    teamLinkDialog.value = true
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: response.message || '团队链接生成成功',
+      life: 3000
+    })
+  } catch (error) {
+    const errorMessage = error?.message || error?.detail || '生成团队链接失败'
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: errorMessage,
+      life: 3000
+    })
+  } finally {
+    generatingLinkId.value = null
+  }
+}
+
+// 查看团队链接
+const handleViewTeamLink = async (team) => {
+  if (!team || !team.id) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '团队信息不存在',
+      life: 3000
+    })
+    return
+  }
+
+  currentTeamIdForLink.value = team.id
+  try {
+    // 从推广链接列表中查找团队的链接(memberId为null的链接)
+    const response = await listLinks(0, 1000, team.id, undefined)
+    const teamLinks = response.content || []
+    
+    // 查找团队的链接(memberId为null,type为GENERAL或BROWSER)
+    const generalLink = teamLinks.find(link => link.memberId === null && link.type === 'GENERAL')
+    const browserLink = teamLinks.find(link => link.memberId === null && link.type === 'BROWSER')
+    
+    if (generalLink || browserLink) {
+      teamLinkData.value = {
+        generalLink: generalLink?.link || null,
+        browserLink: browserLink?.link || null
+      }
+      teamLinkDialog.value = true
+    } else {
+      toast.add({
+        severity: 'warn',
+        summary: '提示',
+        detail: '该团队暂无推广链接,请先生成团队链接',
+        life: 3000
+      })
+    }
+  } catch (error) {
+    const errorMessage = error?.message || error?.detail || '获取团队链接失败'
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: errorMessage,
+      life: 3000
+    })
+  }
+}
+
 // 初始化
 // 初始化
 onMounted(() => {
 onMounted(() => {
   fetchData()
   fetchData()

+ 314 - 0
src/views/VideoFeedbackView.vue

@@ -0,0 +1,314 @@
+<script setup>
+import { ref, onMounted, inject } from 'vue'
+import { submitVideoFeedback, listVideoFeedback } from '@/services/api'
+import { useToast } from 'primevue/usetoast'
+import { useDateFormat } from '@vueuse/core'
+import DataTable from 'primevue/datatable'
+import Column from 'primevue/column'
+import Button from 'primevue/button'
+import Dialog from 'primevue/dialog'
+import InputText from 'primevue/inputtext'
+import InputNumber from 'primevue/inputnumber'
+import Textarea from 'primevue/textarea'
+import { Form } from '@primevue/forms'
+import { zodResolver } from '@primevue/forms/resolvers/zod'
+import { z } from 'zod'
+import FloatLabel from 'primevue/floatlabel'
+import Message from 'primevue/message'
+
+const toast = useToast()
+const isAdmin = inject('isAdmin')
+
+const tableData = ref({
+  list: [],
+  total: 0,
+  page: 0,
+  size: 20
+})
+
+const loading = ref(false)
+const feedbackDialog = ref(false)
+const feedbackFormLoading = ref(false)
+
+const feedbackForm = ref({
+  videoId: null,
+  reason: '',
+  url: ''
+})
+
+const feedbackFormResolver = zodResolver(
+  z.object({
+    videoId: z.number().min(1, { message: '视频ID必须大于0' }),
+    reason: z.string().min(1, { message: '反馈理由不能为空' }),
+    url: z
+      .string()
+      .refine((val) => !val || val.trim() === '' || /^https?:\/\/.+/.test(val), {
+        message: '请输入有效的URL地址(以http://或https://开头)'
+      })
+      .optional()
+  })
+)
+
+// 获取反馈列表
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const response = await listVideoFeedback(tableData.value.page, tableData.value.size)
+    if (response.code === 1 && response.data) {
+      tableData.value = {
+        list: response.data.list || [],
+        total: response.data.total || 0,
+        page: response.data.page || 0,
+        size: response.data.size || 20
+      }
+    } else {
+      tableData.value = {
+        list: [],
+        total: 0,
+        page: 0,
+        size: 20
+      }
+    }
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.msg || '获取反馈列表失败',
+      life: 3000
+    })
+    tableData.value = {
+      list: [],
+      total: 0,
+      page: 0,
+      size: 20
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+// 分页变化
+const handlePageChange = (event) => {
+  tableData.value.page = event.page
+  tableData.value.size = event.rows
+  fetchData()
+}
+
+// 打开添加反馈对话框
+const openAddDialog = () => {
+  feedbackForm.value = {
+    videoId: null,
+    reason: '',
+    url: ''
+  }
+  feedbackDialog.value = true
+}
+
+// 提交反馈
+const submitFeedback = async ({ valid, values }) => {
+  if (!valid) return
+
+  feedbackFormLoading.value = true
+  try {
+    const response = await submitVideoFeedback(values.videoId, values.reason, values.url)
+    if (response.code === 1) {
+      toast.add({
+        severity: 'success',
+        summary: '成功',
+        detail: response.msg || '反馈提交成功',
+        life: 3000
+      })
+      feedbackDialog.value = false
+      fetchData() // 刷新列表
+    } else {
+      toast.add({
+        severity: 'error',
+        summary: '错误',
+        detail: response.msg || '反馈提交失败',
+        life: 3000
+      })
+    }
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.msg || '反馈提交失败',
+      life: 3000
+    })
+  } finally {
+    feedbackFormLoading.value = false
+  }
+}
+
+// 格式化时间戳
+const formatTimestamp = (timestamp) => {
+  if (!timestamp) return '-'
+  return useDateFormat(new Date(timestamp * 1000), 'YYYY-MM-DD HH:mm:ss').value
+}
+
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<template>
+  <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
+    <!-- 权限检查 -->
+    <div v-if="!isAdmin" class="text-center py-8">
+      <p class="text-gray-500">您没有权限访问视频反馈管理</p>
+    </div>
+
+    <!-- 主要内容 -->
+    <div v-else>
+      <DataTable
+        :value="tableData.list"
+        :paginator="true"
+        paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
+        currentPageReportTemplate="{totalRecords} 条记录 "
+        :rows="tableData.size"
+        :rowsPerPageOptions="[10, 20, 50, 100]"
+        :totalRecords="tableData.total"
+        @page="handlePageChange"
+        :loading="loading"
+        lazy
+        scrollable
+        stripedRows
+        showGridlines
+      >
+        <template #header>
+          <div class="flex flex-wrap items-center justify-between gap-2">
+            <div class="flex gap-2">
+              <Button icon="pi pi-refresh" @click="fetchData" size="small" label="刷新" />
+              <Button icon="pi pi-plus" @click="openAddDialog" label="添加反馈" size="small" severity="success" />
+            </div>
+          </div>
+        </template>
+
+        <Column field="videoId" header="视频ID" style="min-width: 100px" headerClass="font-bold">
+          <template #body="slotProps">
+            <span class="font-mono text-sm">{{ slotProps.data.videoId }}</span>
+          </template>
+        </Column>
+
+        <Column field="userId" header="用户ID" style="min-width: 100px" headerClass="font-bold">
+          <template #body="slotProps">
+            <span class="font-mono text-sm">{{ slotProps.data.userId }}</span>
+          </template>
+        </Column>
+
+        <Column field="reason" header="反馈理由" style="min-width: 300px" headerClass="font-bold">
+          <template #body="slotProps">
+            <div class="max-w-md">{{ slotProps.data.reason }}</div>
+          </template>
+        </Column>
+
+        <Column field="url" header="访问链接" style="min-width: 250px" headerClass="font-bold">
+          <template #body="slotProps">
+            <a
+              v-if="slotProps.data.url"
+              :href="slotProps.data.url"
+              target="_blank"
+              rel="noopener noreferrer"
+              class="text-blue-600 hover:text-blue-800 underline break-all"
+            >
+              {{ slotProps.data.url }}
+            </a>
+            <span v-else class="text-gray-400">-</span>
+          </template>
+        </Column>
+
+        <Column field="timestamp" header="反馈时间" style="min-width: 180px" headerClass="font-bold">
+          <template #body="slotProps">
+            {{ formatTimestamp(slotProps.data.timestamp) }}
+          </template>
+        </Column>
+      </DataTable>
+
+      <!-- 添加反馈对话框 -->
+      <Dialog
+        v-model:visible="feedbackDialog"
+        :modal="true"
+        header="添加视频反馈"
+        :style="{ width: '500px' }"
+        position="center"
+      >
+        <Form
+          v-slot="$form"
+          :resolver="feedbackFormResolver"
+          :initialValues="feedbackForm"
+          @submit="submitFeedback"
+          class="p-fluid"
+        >
+          <div class="field mt-4">
+            <FloatLabel variant="on">
+              <InputNumber
+                id="videoId"
+                name="videoId"
+                v-model="feedbackForm.videoId"
+                :min="1"
+                :useGrouping="false"
+                fluid
+              />
+              <label for="videoId">视频ID</label>
+            </FloatLabel>
+            <Message v-if="$form.videoId?.invalid" severity="error" size="small" variant="simple">
+              {{ $form.videoId.error?.message }}
+            </Message>
+          </div>
+
+          <div class="field mt-4">
+            <FloatLabel variant="on">
+              <Textarea
+                id="reason"
+                name="reason"
+                v-model="feedbackForm.reason"
+                rows="5"
+                autoResize
+                fluid
+              />
+              <label for="reason">反馈理由</label>
+            </FloatLabel>
+            <Message v-if="$form.reason?.invalid" severity="error" size="small" variant="simple">
+              {{ $form.reason.error?.message }}
+            </Message>
+          </div>
+
+          <div class="field mt-4">
+            <FloatLabel variant="on">
+              <InputText
+                id="url"
+                name="url"
+                v-model="feedbackForm.url"
+                placeholder="https://example.com"
+                fluid
+              />
+              <label for="url">访问链接(可选)</label>
+            </FloatLabel>
+            <Message v-if="$form.url?.invalid" severity="error" size="small" variant="simple">
+              {{ $form.url.error?.message }}
+            </Message>
+          </div>
+
+          <div class="flex justify-end gap-2 mt-4">
+            <Button
+              label="取消"
+              severity="secondary"
+              type="button"
+              @click="feedbackDialog = false"
+              :disabled="feedbackFormLoading"
+            />
+            <Button label="提交" type="submit" :loading="feedbackFormLoading" />
+          </div>
+        </Form>
+      </Dialog>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.max-w-md {
+  max-width: 500px;
+  word-break: break-word;
+}
+</style>
+