TeamMembersView.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. <template>
  2. <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
  3. <DataTable
  4. :value="tableData.content"
  5. :paginator="true"
  6. paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
  7. currentPageReportTemplate="{totalRecords} 条记录 "
  8. :rows="tableData.metadata.size"
  9. :rowsPerPageOptions="[10, 20, 50, 100]"
  10. :totalRecords="tableData.metadata.total"
  11. @page="handlePageChange"
  12. lazy
  13. scrollable
  14. class="members-table"
  15. >
  16. <template #header>
  17. <div class="flex flex-wrap items-center gap-2">
  18. <InputText
  19. v-if="isAdmin"
  20. v-model="searchForm.id"
  21. placeholder="ID"
  22. size="small"
  23. class="w-32"
  24. @keyup.enter="handleSearch"
  25. />
  26. <InputText
  27. v-model="searchForm.name"
  28. placeholder="成员名称"
  29. size="small"
  30. class="w-32"
  31. @keyup.enter="handleSearch"
  32. />
  33. <Select
  34. v-if="isAdmin"
  35. v-model="searchForm.teamId"
  36. :options="teamOptions"
  37. optionLabel="label"
  38. optionValue="value"
  39. placeholder="选择团队"
  40. size="small"
  41. class="w-40"
  42. showClear
  43. />
  44. <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
  45. <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
  46. <Button icon="pi pi-plus" @click="openAddDialog" label="新增成员" size="small" severity="success" />
  47. <div class="flex-1"></div>
  48. <!-- 团队用户统计信息提示 -->
  49. <div v-if="isTeam" class="text-sm text-gray-600">
  50. <span class="font-medium">团队统计:</span>
  51. <span v-if="teamStats" class="text-green-600 ml-1">已加载</span>
  52. <span v-else class="text-orange-600 ml-1">未加载</span>
  53. </div>
  54. </div>
  55. </template>
  56. <Column field="id" header="ID" style="width: 80px" frozen>
  57. <template #body="slotProps">
  58. <span
  59. class="font-mono text-sm copyable-text"
  60. :title="slotProps.data.id"
  61. @click="copyToClipboard(slotProps.data.id)"
  62. >
  63. {{ slotProps.data.id }}
  64. </span>
  65. </template>
  66. </Column>
  67. <Column field="name" header="成员名称" style="min-width: 150px; max-width: 200px">
  68. <template #body="slotProps">
  69. <span
  70. class="font-medium member-name-text copyable-text"
  71. :title="slotProps.data.name"
  72. @click="copyToClipboard(slotProps.data.name)"
  73. >
  74. {{ slotProps.data.name }}
  75. </span>
  76. </template>
  77. </Column>
  78. <Column v-if="isAdmin" field="teamId" header="所属团队" style="min-width: 120px">
  79. <template #body="slotProps">
  80. <span class="team-name-text font-medium">
  81. {{ getTeamName(slotProps.data.teamId) }}
  82. </span>
  83. </template>
  84. </Column>
  85. <Column field="commissionRate" header="分成比例" style="min-width: 120px">
  86. <template #body="slotProps">
  87. <span class="commission-rate-text font-semibold">
  88. {{ slotProps.data.commissionRate || 0 }}%
  89. </span>
  90. </template>
  91. </Column>
  92. <!-- 团队用户可见的收入统计列 -->
  93. <Column v-if="isTeam" field="totalRevenue" header="总收入" style="min-width: 120px">
  94. <template #body="slotProps">
  95. <span class="total-revenue-text font-semibold text-blue-600">
  96. ¥{{ formatAmount(getMemberTotalRevenue(slotProps.data.id)) }}
  97. </span>
  98. </template>
  99. </Column>
  100. <Column v-if="isTeam" field="todayRevenue" header="今日收入" style="min-width: 120px">
  101. <template #body="slotProps">
  102. <span class="today-revenue-text font-semibold text-green-600">
  103. ¥{{ formatAmount(getMemberTodayRevenue(slotProps.data.id)) }}
  104. </span>
  105. </template>
  106. </Column>
  107. <!-- 团队用户可见的销售额统计列 -->
  108. <Column v-if="isTeam" field="totalSales" header="总销售额" style="min-width: 120px">
  109. <template #body="slotProps">
  110. <span class="total-sales-text font-semibold text-purple-600">
  111. ¥{{ formatAmount(getMemberTotalSales(slotProps.data.id)) }}
  112. </span>
  113. </template>
  114. </Column>
  115. <Column v-if="isTeam" field="todaySales" header="今日销售额" style="min-width: 120px">
  116. <template #body="slotProps">
  117. <span class="today-sales-text font-semibold text-orange-600">
  118. ¥{{ formatAmount(getMemberTodaySales(slotProps.data.id)) }}
  119. </span>
  120. </template>
  121. </Column>
  122. <!-- 实际收入(比例)列 -->
  123. <Column v-if="isTeam" field="actualIncome" header="队长收入(比例)" style="min-width: 150px">
  124. <template #body="slotProps">
  125. <div class="flex flex-col">
  126. <span v-if="isTeamLeader(slotProps.data.id)" class="team-leader-income-text font-semibold text-red-600">
  127. ¥{{ formatAmount(getTeamLeaderTotalIncome(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
  128. </span>
  129. <span v-else class="actual-rate-text font-semibold text-blue-600">
  130. ¥{{ formatAmount(getMemberTotalRevenue(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
  131. </span>
  132. </div>
  133. </template>
  134. </Column>
  135. <!-- 今日实际收入(比例)列 -->
  136. <Column v-if="isTeam" field="todayActualIncome" header="队长今日收入(比例)" style="min-width: 150px">
  137. <template #body="slotProps">
  138. <div class="flex flex-col">
  139. <span v-if="isTeamLeader(slotProps.data.id)" class="team-leader-today-income-text font-semibold text-red-600">
  140. ¥{{ formatAmount(getTeamLeaderTodayIncome(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
  141. </span>
  142. <span v-else class="today-revenue-text font-semibold text-green-600">
  143. ¥{{ formatAmount(getMemberTodayRevenue(slotProps.data.id)) }}({{ getMemberActualRate(slotProps.data.id) }}%)
  144. </span>
  145. </div>
  146. </template>
  147. </Column>
  148. <Column field="createdAt" header="创建时间" style="min-width: 150px">
  149. <template #body="slotProps">
  150. <span class="text-sm">
  151. {{ formatDateTime(slotProps.data.createdAt) }}
  152. </span>
  153. </template>
  154. </Column>
  155. <Column
  156. header="操作"
  157. style="min-width: 280px; width: 280px"
  158. align-frozen="right"
  159. frozen
  160. :pt="{
  161. columnHeaderContent: {
  162. class: 'justify-center'
  163. }
  164. }"
  165. >
  166. <template #body="slotProps">
  167. <div class="flex justify-center gap-1">
  168. <Button
  169. icon="pi pi-link"
  170. severity="success"
  171. size="small"
  172. text
  173. rounded
  174. aria-label="生成链接"
  175. title="生成推广链接"
  176. @click="handleGenerateLink(slotProps.data)"
  177. />
  178. <Button
  179. icon="pi pi-qrcode"
  180. severity="warning"
  181. size="small"
  182. text
  183. rounded
  184. aria-label="生成推广码"
  185. title="生成推广码"
  186. @click="handleGeneratePromoCode(slotProps.data)"
  187. />
  188. <Button
  189. icon="pi pi-pencil"
  190. severity="info"
  191. size="small"
  192. text
  193. rounded
  194. aria-label="编辑"
  195. @click="openEditDialog(slotProps.data)"
  196. />
  197. <Button
  198. icon="pi pi-trash"
  199. severity="danger"
  200. size="small"
  201. text
  202. rounded
  203. aria-label="删除"
  204. @click="confirmDelete(slotProps.data)"
  205. />
  206. </div>
  207. </template>
  208. </Column>
  209. </DataTable>
  210. <!-- 新增/编辑弹窗 -->
  211. <Dialog
  212. v-model:visible="editDialog"
  213. :modal="true"
  214. :header="isEdit ? '编辑成员' : '新增成员'"
  215. :style="{ width: '500px' }"
  216. position="center"
  217. >
  218. <div class="p-fluid">
  219. <div class="field">
  220. <label for="edit-name" class="font-medium text-sm mb-2 block">成员名称</label>
  221. <InputText id="edit-name" v-model="editForm.name" class="w-full" />
  222. </div>
  223. <div v-if="isAdmin" class="field mt-4">
  224. <label for="edit-teamId" class="font-medium text-sm mb-2 block">选择团队</label>
  225. <Select
  226. id="edit-teamId"
  227. v-model="editForm.teamId"
  228. :options="teamSelectOptions"
  229. optionLabel="label"
  230. optionValue="value"
  231. placeholder="请选择团队"
  232. class="w-full"
  233. showClear
  234. />
  235. </div>
  236. <div v-if="!isEdit" class="field mt-4">
  237. <label for="edit-password" class="font-medium text-sm mb-2 block">密码</label>
  238. <Password
  239. id="edit-password"
  240. v-model="editForm.password"
  241. :feedback="false"
  242. toggleMask
  243. class="w-full"
  244. placeholder="请输入密码"
  245. inputClass="w-full"
  246. />
  247. </div>
  248. <div v-if="!isEdit" class="field mt-4">
  249. <label for="edit-confirmPassword" class="font-medium text-sm mb-2 block">确认密码</label>
  250. <Password
  251. id="edit-confirmPassword"
  252. v-model="editForm.confirmPassword"
  253. :feedback="false"
  254. toggleMask
  255. class="w-full"
  256. placeholder="请再次输入密码"
  257. inputClass="w-full"
  258. />
  259. </div>
  260. <div class="field mt-4">
  261. <label for="edit-commissionRate" class="font-medium text-sm mb-2 block">分成比例 (%)</label>
  262. <InputNumber
  263. id="edit-commissionRate"
  264. v-model="editForm.commissionRate"
  265. :min="0"
  266. :max="100"
  267. :step="0.1"
  268. suffix=" %"
  269. class="w-full"
  270. placeholder="请输入分成比例"
  271. :useGrouping="false"
  272. :allowEmpty="true"
  273. :showButtons="false"
  274. />
  275. </div>
  276. </div>
  277. <template #footer>
  278. <div class="flex justify-end gap-3">
  279. <Button label="取消" severity="secondary" @click="editDialog = false" />
  280. <Button label="保存" severity="success" @click="saveEdit" :loading="editLoading" />
  281. </div>
  282. </template>
  283. </Dialog>
  284. <!-- 推广码/链接展示弹窗 -->
  285. <Dialog
  286. v-model:visible="promoDialog"
  287. :modal="true"
  288. :header="promoDialogTitle"
  289. :style="{ width: '500px' }"
  290. position="center"
  291. >
  292. <div class="p-fluid">
  293. <div v-if="promoData.promoCode" class="field">
  294. <label class="font-medium text-sm mb-2 block">推广码</label>
  295. <div class="flex items-center gap-2">
  296. <InputText :value="promoData.promoCode" readonly class="flex-1" />
  297. <Button
  298. icon="pi pi-copy"
  299. size="small"
  300. @click="copyToClipboard(promoData.promoCode)"
  301. title="复制推广码"
  302. />
  303. </div>
  304. </div>
  305. <div v-if="promoData.promotionLink" class="field mt-4">
  306. <label class="font-medium text-sm mb-2 block">推广链接</label>
  307. <div class="flex items-center gap-2">
  308. <InputText :value="promoData.promotionLink" readonly class="flex-1" />
  309. <Button
  310. icon="pi pi-copy"
  311. size="small"
  312. @click="copyToClipboard(promoData.promotionLink)"
  313. title="复制推广链接"
  314. />
  315. </div>
  316. </div>
  317. </div>
  318. <template #footer>
  319. <div class="flex justify-end gap-3">
  320. <Button label="关闭" severity="secondary" @click="promoDialog = false" />
  321. </div>
  322. </template>
  323. </Dialog>
  324. </div>
  325. </template>
  326. <script setup>
  327. import { ref, onMounted, computed, inject } from 'vue'
  328. import Button from 'primevue/button'
  329. import Column from 'primevue/column'
  330. import DataTable from 'primevue/datatable'
  331. import Dialog from 'primevue/dialog'
  332. import Select from 'primevue/select'
  333. import InputText from 'primevue/inputtext'
  334. import InputNumber from 'primevue/inputnumber'
  335. import Password from 'primevue/password'
  336. import { useConfirm } from 'primevue/useconfirm'
  337. import { useToast } from 'primevue/usetoast'
  338. import { listMembers, createMember, updateMember, deleteMember, getTeamLeaderStats, generatePromoCode, getPromotionLink } from '@/services/api'
  339. import { useTeamStore } from '@/stores/team'
  340. const toast = useToast()
  341. const confirm = useConfirm()
  342. const teamStore = useTeamStore()
  343. const isAdmin = inject('isAdmin')
  344. const isTeam = inject('isTeam')
  345. // 团队选项
  346. const teamOptions = computed(() => {
  347. return [
  348. { label: '全部团队', value: null },
  349. ...teamStore.teams.map((team) => ({
  350. label: team.name,
  351. value: team.id
  352. }))
  353. ]
  354. })
  355. const teamSelectOptions = computed(() => {
  356. return teamStore.teams.map((team) => ({
  357. label: team.name,
  358. value: team.id
  359. }))
  360. })
  361. const getTeamName = (teamId) => {
  362. if (!teamId) return '-'
  363. const team = teamStore.teams.find((t) => t.id === teamId)
  364. return team ? team.name : '-'
  365. }
  366. const getTeamUserId = (teamId) => {
  367. if (!teamId) return null
  368. const team = teamStore.teams.find((t) => t.id === teamId)
  369. return team ? team.userId : null
  370. }
  371. // 表格数据
  372. const tableData = ref({
  373. content: [],
  374. metadata: {
  375. page: 0,
  376. size: 20,
  377. total: 0
  378. }
  379. })
  380. // 加载状态
  381. const loading = ref(false)
  382. const statsLoading = ref(false)
  383. // 团队统计数据
  384. const teamStats = ref(null)
  385. // 编辑相关
  386. const editDialog = ref(false)
  387. const editLoading = ref(false)
  388. const isEdit = ref(false)
  389. const editForm = ref({
  390. id: null,
  391. name: null,
  392. teamId: null,
  393. commissionRate: null
  394. })
  395. // 推广码/链接弹窗相关
  396. const promoDialog = ref(false)
  397. const promoDialogTitle = ref('')
  398. const promoData = ref({
  399. promoCode: null,
  400. promotionLink: null
  401. })
  402. // 搜索表单
  403. const searchForm = ref({
  404. id: null,
  405. name: null,
  406. teamId: null
  407. })
  408. // 格式化金额
  409. const formatAmount = (amount) => {
  410. if (!amount) return '0.00'
  411. return Number(amount).toFixed(2)
  412. }
  413. // 格式化日期时间
  414. const formatDateTime = (dateString) => {
  415. if (!dateString) return '-'
  416. const date = new Date(dateString)
  417. const year = date.getFullYear()
  418. const month = String(date.getMonth() + 1).padStart(2, '0')
  419. const day = String(date.getDate()).padStart(2, '0')
  420. const hours = String(date.getHours()).padStart(2, '0')
  421. const minutes = String(date.getMinutes()).padStart(2, '0')
  422. const seconds = String(date.getSeconds()).padStart(2, '0')
  423. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  424. }
  425. // 获取成员的实际比例
  426. const getMemberActualRate = (memberId) => {
  427. if (!teamStats.value?.membersStats) return 0
  428. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  429. return member?.actualRate || 0
  430. }
  431. // 获取成员的总收入
  432. const getMemberTotalRevenue = (memberId) => {
  433. if (!teamStats.value?.membersStats) return 0
  434. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  435. return member?.totalRevenue || 0
  436. }
  437. // 获取成员的今日收入
  438. const getMemberTodayRevenue = (memberId) => {
  439. if (!teamStats.value?.membersStats) return 0
  440. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  441. return member?.todayRevenue || 0
  442. }
  443. // 获取成员的总销售额
  444. const getMemberTotalSales = (memberId) => {
  445. if (!teamStats.value?.membersStats) return 0
  446. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  447. return member?.totalSales || 0
  448. }
  449. // 获取成员的今日销售额
  450. const getMemberTodaySales = (memberId) => {
  451. if (!teamStats.value?.membersStats) return 0
  452. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  453. return member?.todaySales || 0
  454. }
  455. // 判断是否为队长
  456. const isTeamLeader = (memberId) => {
  457. if (!teamStats.value?.membersStats) return false
  458. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  459. return member?.teamLeaderTotalIncome !== undefined || member?.teamLeaderTodayIncome !== undefined
  460. }
  461. // 获取队长的实际总收入
  462. const getTeamLeaderTotalIncome = (memberId) => {
  463. if (!teamStats.value?.membersStats) return 0
  464. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  465. return member?.teamLeaderTotalIncome || 0
  466. }
  467. // 获取队长的实际今日收入
  468. const getTeamLeaderTodayIncome = (memberId) => {
  469. if (!teamStats.value?.membersStats) return 0
  470. const member = teamStats.value.membersStats.find(m => m.memberId === memberId)
  471. return member?.teamLeaderTodayIncome || 0
  472. }
  473. // 获取数据
  474. const fetchData = async () => {
  475. loading.value = true
  476. try {
  477. const response = await listMembers(
  478. tableData.value.metadata.page,
  479. tableData.value.metadata.size,
  480. searchForm.value.name || undefined,
  481. searchForm.value.teamId || undefined
  482. )
  483. tableData.value = response
  484. } catch {
  485. toast.add({
  486. severity: 'error',
  487. summary: '错误',
  488. detail: '获取成员列表失败',
  489. life: 3000
  490. })
  491. } finally {
  492. loading.value = false
  493. }
  494. }
  495. // 获取团队统计数据
  496. const fetchTeamStats = async () => {
  497. if (!isTeam.value) return
  498. statsLoading.value = true
  499. try {
  500. const response = await getTeamLeaderStats()
  501. teamStats.value = response
  502. } catch (error) {
  503. console.error('获取团队统计数据失败:', error)
  504. if (error.status === 403) {
  505. toast.add({
  506. severity: 'warn',
  507. summary: '权限不足',
  508. detail: '只有团队用户才能访问统计数据',
  509. life: 3000
  510. })
  511. } else {
  512. toast.add({
  513. severity: 'error',
  514. summary: '错误',
  515. detail: '获取团队统计数据失败',
  516. life: 3000
  517. })
  518. }
  519. } finally {
  520. statsLoading.value = false
  521. }
  522. }
  523. // 分页处理
  524. const handlePageChange = (event) => {
  525. tableData.value.metadata.page = event.page
  526. tableData.value.metadata.size = event.rows
  527. fetchData()
  528. }
  529. // 搜索处理
  530. const handleSearch = () => {
  531. tableData.value.metadata.page = 0
  532. fetchData()
  533. }
  534. // 刷新处理
  535. const handleRefresh = () => {
  536. searchForm.value = {
  537. id: null,
  538. name: null,
  539. teamId: null
  540. }
  541. tableData.value.metadata.page = 0
  542. fetchData()
  543. }
  544. // 确认删除
  545. const confirmDelete = (member) => {
  546. confirm.require({
  547. message: `确定要删除成员 "${member.name}" 吗?`,
  548. header: '确认删除',
  549. icon: 'pi pi-exclamation-triangle',
  550. accept: () => deleteMemberRecord(member.id)
  551. })
  552. }
  553. // 删除成员
  554. const deleteMemberRecord = async (id) => {
  555. try {
  556. await deleteMember(id)
  557. toast.add({
  558. severity: 'success',
  559. summary: '成功',
  560. detail: '删除成功',
  561. life: 3000
  562. })
  563. fetchData()
  564. } catch (error) {
  565. const errorMessage = error?.message || error?.detail || '删除失败'
  566. toast.add({
  567. severity: 'error',
  568. summary: '错误',
  569. detail: errorMessage,
  570. life: 3000
  571. })
  572. }
  573. }
  574. // 复制到剪贴板
  575. const copyToClipboard = async (text) => {
  576. try {
  577. await navigator.clipboard.writeText(text)
  578. toast.add({
  579. severity: 'success',
  580. summary: '成功',
  581. detail: '已复制到剪贴板',
  582. life: 2000
  583. })
  584. } catch {
  585. const textArea = document.createElement('textarea')
  586. textArea.value = text
  587. document.body.appendChild(textArea)
  588. textArea.select()
  589. document.execCommand('copy')
  590. document.body.removeChild(textArea)
  591. toast.add({
  592. severity: 'success',
  593. summary: '成功',
  594. detail: '已复制到剪贴板',
  595. life: 2000
  596. })
  597. }
  598. }
  599. // 打开新增弹窗
  600. const openAddDialog = () => {
  601. isEdit.value = false
  602. editForm.value = {
  603. id: null,
  604. name: null,
  605. teamId: null,
  606. commissionRate: null
  607. }
  608. editDialog.value = true
  609. }
  610. // 打开编辑弹窗
  611. const openEditDialog = (member) => {
  612. isEdit.value = true
  613. editForm.value = {
  614. id: member.id,
  615. name: member.name || null,
  616. teamId: member.teamId || null,
  617. commissionRate: member.commissionRate || null
  618. }
  619. editDialog.value = true
  620. }
  621. // 保存编辑
  622. const saveEdit = async () => {
  623. editLoading.value = true
  624. try {
  625. const formData = {}
  626. if (editForm.value.name !== null && editForm.value.name !== '') {
  627. formData.name = editForm.value.name
  628. }
  629. if (editForm.value.commissionRate !== null && editForm.value.commissionRate !== '') {
  630. formData.commissionRate = editForm.value.commissionRate
  631. }
  632. if (editForm.value.teamId !== null && editForm.value.teamId !== '') {
  633. formData.teamId = editForm.value.teamId
  634. // 只有在创建新成员时才设置userId,编辑时保持原有userId
  635. if (!isEdit.value) {
  636. formData.userId = getTeamUserId(editForm.value.teamId)
  637. }
  638. }
  639. if (isEdit.value) {
  640. await updateMember(editForm.value.id, formData)
  641. } else {
  642. await createMember(formData)
  643. }
  644. toast.add({
  645. severity: 'success',
  646. summary: '成功',
  647. detail: isEdit.value ? '更新成功' : '创建成功',
  648. life: 3000
  649. })
  650. editDialog.value = false
  651. fetchData()
  652. } catch (error) {
  653. const errorMessage = error?.message || error?.detail || (isEdit.value ? '更新失败' : '创建失败')
  654. toast.add({
  655. severity: 'error',
  656. summary: '错误',
  657. detail: errorMessage,
  658. life: 3000
  659. })
  660. } finally {
  661. editLoading.value = false
  662. }
  663. }
  664. // 生成推广码
  665. const handleGeneratePromoCode = async (member) => {
  666. try {
  667. const response = await generatePromoCode(member.id)
  668. promoData.value = {
  669. promoCode: response.promoCode,
  670. promotionLink: null
  671. }
  672. promoDialogTitle.value = `推广码 - ${member.name}`
  673. promoDialog.value = true
  674. toast.add({
  675. severity: 'success',
  676. summary: '成功',
  677. detail: response.message || '推广码生成成功',
  678. life: 3000
  679. })
  680. // 刷新数据以更新推广码
  681. fetchData()
  682. } catch (error) {
  683. const errorMessage = error?.message || error?.detail || '生成推广码失败'
  684. toast.add({
  685. severity: 'error',
  686. summary: '错误',
  687. detail: errorMessage,
  688. life: 3000
  689. })
  690. }
  691. }
  692. // 生成推广链接
  693. const handleGenerateLink = async (member) => {
  694. try {
  695. const response = await getPromotionLink(member.id)
  696. promoData.value = {
  697. promoCode: response.promoCode,
  698. promotionLink: response.promotionLink
  699. }
  700. promoDialogTitle.value = `推广链接 - ${member.name}`
  701. promoDialog.value = true
  702. toast.add({
  703. severity: 'success',
  704. summary: '成功',
  705. detail: '获取推广链接成功',
  706. life: 3000
  707. })
  708. } catch (error) {
  709. const errorMessage = error?.message || error?.detail || '获取推广链接失败'
  710. toast.add({
  711. severity: 'error',
  712. summary: '错误',
  713. detail: errorMessage,
  714. life: 3000
  715. })
  716. }
  717. }
  718. onMounted(() => {
  719. fetchData()
  720. // 如果是团队用户,预加载统计数据
  721. if (isTeam.value) {
  722. fetchTeamStats()
  723. }
  724. })
  725. </script>
  726. <style scoped>
  727. .p-datatable-sm .p-datatable-tbody > tr > td {
  728. padding: 0.5rem;
  729. vertical-align: top;
  730. }
  731. .p-datatable-sm .p-datatable-thead > tr > th {
  732. padding: 0.5rem;
  733. }
  734. .members-table {
  735. width: 100%;
  736. }
  737. .members-table .p-datatable-wrapper {
  738. overflow-x: auto;
  739. }
  740. .members-table .p-datatable-thead th {
  741. white-space: nowrap;
  742. min-width: 100px;
  743. }
  744. .font-mono {
  745. font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  746. }
  747. .member-name-text {
  748. word-wrap: break-word;
  749. word-break: break-all;
  750. white-space: normal;
  751. line-height: 1.4;
  752. }
  753. .team-name-text {
  754. color: #7c3aed;
  755. font-weight: 500;
  756. }
  757. .team-id-text {
  758. word-wrap: break-word;
  759. word-break: break-all;
  760. white-space: normal;
  761. line-height: 1.4;
  762. }
  763. .user-id-text {
  764. word-wrap: break-word;
  765. word-break: break-all;
  766. white-space: normal;
  767. line-height: 1.4;
  768. }
  769. .total-revenue-text {
  770. color: #2563eb;
  771. font-weight: 600;
  772. }
  773. .today-revenue-text {
  774. color: #059669;
  775. font-weight: 600;
  776. }
  777. .commission-rate-text {
  778. color: #7c3aed;
  779. font-weight: 600;
  780. }
  781. .actual-rate-text {
  782. color: #2563eb;
  783. font-weight: 600;
  784. }
  785. .total-sales-text {
  786. color: #7c3aed;
  787. font-weight: 600;
  788. }
  789. .today-sales-text {
  790. color: #ea580c;
  791. font-weight: 600;
  792. }
  793. .team-leader-income-text {
  794. color: #dc2626;
  795. font-weight: 600;
  796. }
  797. .team-leader-today-income-text {
  798. color: #dc2626;
  799. font-weight: 600;
  800. }
  801. .font-medium {
  802. font-weight: 500;
  803. }
  804. .text-sm {
  805. font-size: 0.875rem;
  806. }
  807. .copyable-text {
  808. cursor: pointer;
  809. transition: all 0.2s ease;
  810. user-select: none;
  811. }
  812. .copyable-text:hover {
  813. background-color: #e5e7eb;
  814. border-radius: 4px;
  815. }
  816. .copyable-text:active {
  817. background-color: #d1d5db;
  818. transform: scale(0.98);
  819. }
  820. /* Password组件样式修复 */
  821. .p-password {
  822. width: 100%;
  823. }
  824. .p-password .p-inputtext {
  825. width: 100%;
  826. }
  827. .p-password .p-password-input {
  828. width: 100%;
  829. }
  830. </style>