|
@@ -61,15 +61,49 @@
|
|
|
<span class="text-gray-500 text-sm mb-1">分成比例</span>
|
|
<span class="text-gray-500 text-sm mb-1">分成比例</span>
|
|
|
<span class="text-base font-bold text-purple-700">{{ memberData.commissionRate }}%</span>
|
|
<span class="text-base font-bold text-purple-700">{{ memberData.commissionRate }}%</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="flex flex-col">
|
|
|
|
|
|
|
+ <div class="flex flex-col sm:col-span-2">
|
|
|
<span class="text-gray-500 text-sm mb-1">推广码</span>
|
|
<span class="text-gray-500 text-sm mb-1">推广码</span>
|
|
|
- <span
|
|
|
|
|
- class="text-base font-bold text-orange-600 font-mono copyable-text cursor-pointer"
|
|
|
|
|
- :title="memberData.affCode || '-'"
|
|
|
|
|
- @click="memberData.affCode && copyToClipboard(memberData.affCode)"
|
|
|
|
|
- >
|
|
|
|
|
- {{ memberData.affCode || '-' }}
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ <span
|
|
|
|
|
+ class="text-base font-bold text-orange-600 font-mono copyable-text cursor-pointer flex-1"
|
|
|
|
|
+ :title="memberData.promoCode || memberData.affCode || '-'"
|
|
|
|
|
+ @click="(memberData.promoCode || memberData.affCode) && copyToClipboard(memberData.promoCode || memberData.affCode)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ memberData.promoCode || memberData.affCode || '-' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-qrcode"
|
|
|
|
|
+ severity="warning"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ text
|
|
|
|
|
+ rounded
|
|
|
|
|
+ title="生成推广码"
|
|
|
|
|
+ @click="handleGeneratePromoCode"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-col sm:col-span-2">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">推广链接</span>
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="promoLink"
|
|
|
|
|
+ class="text-base font-bold text-blue-600 font-mono copyable-text cursor-pointer flex-1 truncate"
|
|
|
|
|
+ :title="promoLink"
|
|
|
|
|
+ @click="copyToClipboard(promoLink)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ promoLink }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span v-else class="text-base text-gray-400 flex-1">-</span>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-link"
|
|
|
|
|
+ severity="success"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ text
|
|
|
|
|
+ rounded
|
|
|
|
|
+ title="生成推广链接"
|
|
|
|
|
+ @click="handleGenerateLink"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="flex flex-col">
|
|
<div class="flex flex-col">
|
|
|
<span class="text-gray-500 text-sm mb-1">总用户数</span>
|
|
<span class="text-gray-500 text-sm mb-1">总用户数</span>
|
|
@@ -317,6 +351,49 @@
|
|
|
</template>
|
|
</template>
|
|
|
</Dialog>
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
+ <!-- 推广码/链接展示弹窗(推广员) -->
|
|
|
|
|
+ <Dialog
|
|
|
|
|
+ v-model:visible="promoDialog"
|
|
|
|
|
+ :modal="true"
|
|
|
|
|
+ :header="promoDialogTitle"
|
|
|
|
|
+ :style="{ width: '500px' }"
|
|
|
|
|
+ position="center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="p-fluid">
|
|
|
|
|
+ <div v-if="promoData.promoCode" class="field">
|
|
|
|
|
+ <label class="font-medium text-sm mb-2 block">推广码</label>
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ <InputText :value="promoData.promoCode" readonly class="flex-1" />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-copy"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @click="copyToClipboard(promoData.promoCode)"
|
|
|
|
|
+ title="复制推广码"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="promoData.promotionLink" class="field mt-4">
|
|
|
|
|
+ <label class="font-medium text-sm mb-2 block">推广链接</label>
|
|
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
|
|
+ <InputText :value="promoData.promotionLink" readonly class="flex-1" />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-copy"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ @click="copyToClipboard(promoData.promotionLink)"
|
|
|
|
|
+ title="复制推广链接"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
|
|
+ <Button label="关闭" severity="secondary" @click="promoDialog = false" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
<!-- 团队数据表格 (仅管理员可见) -->
|
|
<!-- 团队数据表格 (仅管理员可见) -->
|
|
|
<div v-if="isAdmin" class="teams-table-container">
|
|
<div v-if="isAdmin" class="teams-table-container">
|
|
|
<h2>团队数据一览</h2>
|
|
<h2>团队数据一览</h2>
|
|
@@ -362,7 +439,7 @@
|
|
|
import { ref, onMounted, onUnmounted, computed, inject, watch } from 'vue'
|
|
import { ref, onMounted, onUnmounted, computed, inject, watch } from 'vue'
|
|
|
import { useUserStore } from '@/stores/user'
|
|
import { useUserStore } from '@/stores/user'
|
|
|
import { useTeamStore } from '@/stores/team'
|
|
import { useTeamStore } from '@/stores/team'
|
|
|
-import { getAllTeamStatistics, getIncomeStatistics, listMembers, getTeamIpConversionRate, listTeams, getTeamDomainDailyStatistics, getTeamDomainAllStatistics, updateTeamThemeColor } from '@/services/api'
|
|
|
|
|
|
|
+import { getAllTeamStatistics, getIncomeStatistics, listMembers, getTeamIpConversionRate, listTeams, getTeamDomainDailyStatistics, getTeamDomainAllStatistics, updateTeamThemeColor, generatePromoCode, getPromotionLink, getMyPromotionInfo, generateMyPromoCode, getTeamMemberIpConversionRate, getTeamMemberAllStatistics, getTeamMemberDailyStatistics } from '@/services/api'
|
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { useToast } from 'primevue/usetoast'
|
|
|
import Chart from 'chart.js/auto'
|
|
import Chart from 'chart.js/auto'
|
|
|
import Select from 'primevue/select'
|
|
import Select from 'primevue/select'
|
|
@@ -370,6 +447,7 @@ import DataTable from 'primevue/datatable'
|
|
|
import Column from 'primevue/column'
|
|
import Column from 'primevue/column'
|
|
|
import Button from 'primevue/button'
|
|
import Button from 'primevue/button'
|
|
|
import Dialog from 'primevue/dialog'
|
|
import Dialog from 'primevue/dialog'
|
|
|
|
|
+import InputText from 'primevue/inputtext'
|
|
|
|
|
|
|
|
const userStore = useUserStore()
|
|
const userStore = useUserStore()
|
|
|
const teamStore = useTeamStore()
|
|
const teamStore = useTeamStore()
|
|
@@ -391,6 +469,13 @@ const chartInstance = ref(null)
|
|
|
const selectedChartTeamId = ref(null) // 用于图表的团队选择
|
|
const selectedChartTeamId = ref(null) // 用于图表的团队选择
|
|
|
const ipStats = ref(null) // IP成交率数据
|
|
const ipStats = ref(null) // IP成交率数据
|
|
|
const teamListData = ref(null) // 团队列表数据(包含基础信息如affCode等)
|
|
const teamListData = ref(null) // 团队列表数据(包含基础信息如affCode等)
|
|
|
|
|
+const promoLink = ref(null) // 推广链接
|
|
|
|
|
+const promoDialog = ref(false) // 推广码/链接弹窗
|
|
|
|
|
+const promoDialogTitle = ref('')
|
|
|
|
|
+const promoData = ref({
|
|
|
|
|
+ promoCode: null,
|
|
|
|
|
+ promotionLink: null
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
// 主题选择相关
|
|
// 主题选择相关
|
|
|
const themeDialog = ref(false)
|
|
const themeDialog = ref(false)
|
|
@@ -551,9 +636,11 @@ const teamData = computed(() => {
|
|
|
const memberData = computed(() => {
|
|
const memberData = computed(() => {
|
|
|
if (isPromoter.value && memberStats.value) {
|
|
if (isPromoter.value && memberStats.value) {
|
|
|
return {
|
|
return {
|
|
|
|
|
+ id: memberStats.value.id || null, // 成员ID,用于生成推广码和链接
|
|
|
name: memberStats.value.name || '-',
|
|
name: memberStats.value.name || '-',
|
|
|
commissionRate: memberStats.value.commissionRate || 0,
|
|
commissionRate: memberStats.value.commissionRate || 0,
|
|
|
- affCode: memberStats.value.affCode || null,
|
|
|
|
|
|
|
+ promoCode: memberStats.value.promoCode || null, // 使用promoCode
|
|
|
|
|
+ affCode: memberStats.value.affCode || null, // 保留用于向后兼容
|
|
|
todayRevenue: memberStats.value.todayRevenue || memberStats.value.todayCommission || 0,
|
|
todayRevenue: memberStats.value.todayRevenue || memberStats.value.todayCommission || 0,
|
|
|
totalRevenue: memberStats.value.totalRevenue || memberStats.value.totalCommission || 0,
|
|
totalRevenue: memberStats.value.totalRevenue || memberStats.value.totalCommission || 0,
|
|
|
todayCommission: memberStats.value.todayCommission || 0,
|
|
todayCommission: memberStats.value.todayCommission || 0,
|
|
@@ -627,11 +714,81 @@ const copyToClipboard = async (text) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 生成推广码(推广员)
|
|
|
|
|
+const handleGeneratePromoCode = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await generateMyPromoCode()
|
|
|
|
|
+ promoData.value = {
|
|
|
|
|
+ promoCode: response.promoCode,
|
|
|
|
|
+ promotionLink: response.promotionLink || null
|
|
|
|
|
+ }
|
|
|
|
|
+ promoDialogTitle.value = `推广码 - ${memberData.value.name}`
|
|
|
|
|
+ promoDialog.value = true
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: response.message || '推广码生成成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ // 刷新数据以更新推广码
|
|
|
|
|
+ await loadTeamStats()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ const errorMessage = error?.message || error?.detail || '生成推广码失败'
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: errorMessage,
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 生成推广链接(推广员)
|
|
|
|
|
+const handleGenerateLink = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await getMyPromotionInfo()
|
|
|
|
|
+ if (response.promotionLink) {
|
|
|
|
|
+ promoLink.value = response.promotionLink
|
|
|
|
|
+ promoData.value = {
|
|
|
|
|
+ promoCode: response.promoCode,
|
|
|
|
|
+ promotionLink: response.promotionLink
|
|
|
|
|
+ }
|
|
|
|
|
+ promoDialogTitle.value = `推广链接 - ${memberData.value.name}`
|
|
|
|
|
+ promoDialog.value = true
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '获取推广链接成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } 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
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 加载IP成交率数据
|
|
// 加载IP成交率数据
|
|
|
const loadIpConversionRate = async () => {
|
|
const loadIpConversionRate = async () => {
|
|
|
if (!isTeam.value) return
|
|
if (!isTeam.value) return
|
|
|
try {
|
|
try {
|
|
|
- const data = await getTeamIpConversionRate()
|
|
|
|
|
|
|
+ // 优先使用基于personalAgentId的新接口,如果失败则回退到旧接口
|
|
|
|
|
+ const data = await getTeamMemberIpConversionRate().catch(() => {
|
|
|
|
|
+ // 如果新接口失败,尝试使用旧接口(基于domainId)
|
|
|
|
|
+ return getTeamIpConversionRate()
|
|
|
|
|
+ })
|
|
|
ipStats.value = data
|
|
ipStats.value = data
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
// 团队角色无权限外的错误提示
|
|
// 团队角色无权限外的错误提示
|
|
@@ -732,50 +889,54 @@ const loadTeamStats = async () => {
|
|
|
}
|
|
}
|
|
|
teamStats.value = statsData
|
|
teamStats.value = statsData
|
|
|
} else if (isPromoter.value) {
|
|
} else if (isPromoter.value) {
|
|
|
- // 推广员角色加载个人统计数据,使用团队成员列表接口和域名统计接口
|
|
|
|
|
|
|
+ // 推广员角色加载个人统计数据,使用基于personalAgentId的新接口
|
|
|
const currentUserId = userStore.userInfo?.id
|
|
const currentUserId = userStore.userInfo?.id
|
|
|
if (currentUserId) {
|
|
if (currentUserId) {
|
|
|
- // 并行请求团队成员数据、域名统计数据和IP成交率数据
|
|
|
|
|
|
|
+ // 并行请求团队成员基础信息、新统计接口和推广信息
|
|
|
const promises = [
|
|
const promises = [
|
|
|
listMembers(0, 1, undefined, undefined, currentUserId),
|
|
listMembers(0, 1, undefined, undefined, currentUserId),
|
|
|
- getTeamDomainDailyStatistics(),
|
|
|
|
|
- getTeamDomainAllStatistics(),
|
|
|
|
|
- getTeamIpConversionRate().catch(() => null) // 如果接口不支持推广员,返回null
|
|
|
|
|
|
|
+ getTeamMemberDailyStatistics().catch(() => null), // 每日统计(基于personalAgentId)
|
|
|
|
|
+ getTeamMemberAllStatistics().catch(() => null), // 全部统计(基于personalAgentId)
|
|
|
|
|
+ getTeamMemberIpConversionRate().catch(() => null), // IP成交率(基于personalAgentId)
|
|
|
|
|
+ getMyPromotionInfo().catch(() => null) // 获取推广信息,如果失败返回null
|
|
|
]
|
|
]
|
|
|
- const [membersResponse, dailyStatsResponse, allStatsResponse, ipStatsData] = await Promise.all(promises)
|
|
|
|
|
|
|
+ const [membersResponse, dailyStatsResponse, allStatsResponse, ipStatsData, promotionInfo] = await Promise.all(promises)
|
|
|
|
|
|
|
|
- // 从列表接口返回的数据中提取第一个成员(当前用户)的统计数据
|
|
|
|
|
|
|
+ // 从列表接口返回的数据中提取第一个成员(当前用户)的基础信息
|
|
|
let memberDataItem = null
|
|
let memberDataItem = null
|
|
|
if (membersResponse.content && membersResponse.content.length > 0) {
|
|
if (membersResponse.content && membersResponse.content.length > 0) {
|
|
|
memberDataItem = membersResponse.content[0]
|
|
memberDataItem = membersResponse.content[0]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 从域名统计数据中提取(绑定的只返回一条数据)
|
|
|
|
|
- const dailyStats = Array.isArray(dailyStatsResponse) && dailyStatsResponse.length > 0 ? dailyStatsResponse[0] : null
|
|
|
|
|
- const allStats = Array.isArray(allStatsResponse) && allStatsResponse.length > 0 ? allStatsResponse[0] : null
|
|
|
|
|
-
|
|
|
|
|
- // 合并数据,优先使用域名统计数据
|
|
|
|
|
|
|
+ // 合并数据,使用新接口返回的统计数据
|
|
|
memberStats.value = {
|
|
memberStats.value = {
|
|
|
|
|
+ id: memberDataItem?.id || null, // 保存成员ID,用于生成推广码和链接
|
|
|
name: memberDataItem?.name || '-',
|
|
name: memberDataItem?.name || '-',
|
|
|
commissionRate: memberDataItem?.commissionRate || 0,
|
|
commissionRate: memberDataItem?.commissionRate || 0,
|
|
|
- affCode: memberDataItem?.affCode || null,
|
|
|
|
|
- // 优先使用域名统计数据,如果没有则使用成员数据
|
|
|
|
|
- todayRevenue: dailyStats?.todayIncome || memberDataItem?.todayRevenue || 0,
|
|
|
|
|
- totalRevenue: allStats?.totalIncome || memberDataItem?.totalRevenue || 0,
|
|
|
|
|
- todayCommission: dailyStats?.todayIncome || memberDataItem?.todayRevenue || 0,
|
|
|
|
|
- totalCommission: allStats?.totalIncome || memberDataItem?.totalRevenue || 0,
|
|
|
|
|
- todaySales: dailyStats?.todaySales || memberDataItem?.todaySales || 0,
|
|
|
|
|
- totalSales: allStats?.totalSales || memberDataItem?.totalSales || 0,
|
|
|
|
|
- todayDAU: dailyStats?.todayActiveUsers || allStats?.todayActiveUsers || 0,
|
|
|
|
|
- totalUsers: allStats?.totalNewUsers || 0,
|
|
|
|
|
- todayNewUsers: dailyStats?.todayNewUsers || 0,
|
|
|
|
|
- // IP成交率数据
|
|
|
|
|
|
|
+ promoCode: promotionInfo?.promoCode || memberDataItem?.promoCode || null, // 优先使用推广信息接口返回的promoCode
|
|
|
|
|
+ affCode: memberDataItem?.affCode || null, // 保留affCode用于向后兼容
|
|
|
|
|
+ // 使用新接口返回的统计数据(基于personalAgentId)
|
|
|
|
|
+ todayRevenue: dailyStatsResponse?.todayIncome || 0, // 今日收入(personalIncomeAmount)
|
|
|
|
|
+ totalRevenue: allStatsResponse?.totalIncome || dailyStatsResponse?.totalIncome || 0, // 总收入(personalIncomeAmount)
|
|
|
|
|
+ todayCommission: dailyStatsResponse?.todayIncome || 0, // 今日分成(personalIncomeAmount)
|
|
|
|
|
+ totalCommission: allStatsResponse?.totalIncome || dailyStatsResponse?.totalIncome || 0, // 总分成(personalIncomeAmount)
|
|
|
|
|
+ todaySales: dailyStatsResponse?.todaySales || 0, // 今日销售额(orderPrice)
|
|
|
|
|
+ totalSales: allStatsResponse?.totalSales || dailyStatsResponse?.totalSales || 0, // 总销售额(orderPrice)
|
|
|
|
|
+ todayDAU: dailyStatsResponse?.todayActiveUsers || allStatsResponse?.todayActiveUsers || 0, // 今日活跃用户数
|
|
|
|
|
+ totalUsers: allStatsResponse?.totalNewUsers || ipStatsData?.totalUsers || 0, // 总用户数
|
|
|
|
|
+ todayNewUsers: dailyStatsResponse?.todayNewUsers || 0, // 今日新增用户数
|
|
|
|
|
+ // IP成交率数据(基于personalAgentId)
|
|
|
todayIpConversionRate: ipStatsData?.todayIpConversionRate || null,
|
|
todayIpConversionRate: ipStatsData?.todayIpConversionRate || null,
|
|
|
totalIpConversionRate: ipStatsData?.totalIpConversionRate || null,
|
|
totalIpConversionRate: ipStatsData?.totalIpConversionRate || null,
|
|
|
totalOrders: 0, // 列表接口没有返回订单数,设为0
|
|
totalOrders: 0, // 列表接口没有返回订单数,设为0
|
|
|
todayOrders: 0 // 列表接口没有返回订单数,设为0
|
|
todayOrders: 0 // 列表接口没有返回订单数,设为0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 如果推广信息接口返回了推广链接,保存它
|
|
|
|
|
+ if (promotionInfo?.promotionLink) {
|
|
|
|
|
+ promoLink.value = promotionInfo.promotionLink
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 保存IP成交率数据到ipStats,供模板使用
|
|
// 保存IP成交率数据到ipStats,供模板使用
|
|
|
if (ipStatsData) {
|
|
if (ipStatsData) {
|
|
|
ipStats.value = ipStatsData
|
|
ipStats.value = ipStatsData
|