|
@@ -40,67 +40,208 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 队长视图 -->
|
|
|
|
|
- <div v-else-if="isTeam" class="team-stats">
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>团队名称</h3>
|
|
|
|
|
- <p class="stat-value">{{ teamStats?.name || '-' }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card revenue-card">
|
|
|
|
|
- <h3>总分润收入</h3>
|
|
|
|
|
- <p class="stat-value revenue-value">{{ formatAmount(teamStats?.totalRevenue) }}</p>
|
|
|
|
|
- <p class="stat-label">实际收入</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card sales-card">
|
|
|
|
|
- <h3>总售卖金额</h3>
|
|
|
|
|
- <p class="stat-value sales-value">{{ formatAmount(teamStats?.totalSales) }}</p>
|
|
|
|
|
- <p class="stat-label">订单金额</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card revenue-card">
|
|
|
|
|
- <h3>今日分润收入</h3>
|
|
|
|
|
- <p class="stat-value revenue-value">{{ formatAmount(teamStats?.todayRevenue) }}</p>
|
|
|
|
|
- <p class="stat-label">今日实际收入</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card sales-card">
|
|
|
|
|
- <h3>今日售卖金额</h3>
|
|
|
|
|
- <p class="stat-value sales-value">{{ formatAmount(teamStats?.todaySales) }}</p>
|
|
|
|
|
- <p class="stat-label">今日订单金额</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card user-card">
|
|
|
|
|
- <h3>今日日活用户</h3>
|
|
|
|
|
- <p class="stat-value user-value">{{ teamStats?.todayDAU || 0 }}</p>
|
|
|
|
|
- <p class="stat-label">活跃用户数</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card new-user-card">
|
|
|
|
|
- <h3>今日新增用户</h3>
|
|
|
|
|
- <p class="stat-value new-user-value">{{ teamStats?.todayNewUsers || 0 }}</p>
|
|
|
|
|
- <p class="stat-label">新注册用户</p>
|
|
|
|
|
|
|
+ <!-- 队长视图 - 隐藏原来的内容 -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 推广员视图 - 隐藏原来的内容 -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 推广员视图 - 与团队用户保持一致 -->
|
|
|
|
|
+ <div v-if="isPromoter && memberData" class="py-2">
|
|
|
|
|
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
|
|
+ <!-- 左侧:基本信息卡片(与右侧均分) -->
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
|
|
|
|
|
+ <div class="text-xl font-extrabold mb-4 text-gray-900">我的信息</div>
|
|
|
|
|
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">队员姓名</span>
|
|
|
|
|
+ <span class="text-base font-bold text-gray-900">{{ memberData.name }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">分成比例</span>
|
|
|
|
|
+ <span class="text-base font-bold text-purple-700">{{ memberData.commissionRate }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">总用户数</span>
|
|
|
|
|
+ <span class="text-lg font-extrabold text-teal-600">{{ memberData.totalUsers || 0 }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>佣金率</h3>
|
|
|
|
|
- <p class="stat-value">{{ formatRate(teamStats?.averageCommissionRate) }}%</p>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧:统计快照(与左侧均分) -->
|
|
|
|
|
+ <div class="grid grid-cols-2 gap-4">
|
|
|
|
|
+ <div class="stat-card stat-yellow">
|
|
|
|
|
+ <div class="icon-badge icon-yellow"><i class="pi pi-dollar"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日分润收入</div>
|
|
|
|
|
+ <div class="stat-value text-yellow-700">¥{{ formatAmount(memberData.todayRevenue || memberData.todayCommission) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-blue">
|
|
|
|
|
+ <div class="icon-badge icon-blue"><i class="pi pi-wallet"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总分润收入</div>
|
|
|
|
|
+ <div class="stat-value text-blue-700">¥{{ formatAmount(memberData.totalRevenue || memberData.totalCommission) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-green">
|
|
|
|
|
+ <div class="icon-badge icon-green"><i class="pi pi-shopping-cart"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日销售额</div>
|
|
|
|
|
+ <div class="stat-value text-green-700">¥{{ formatAmount(memberData.todaySales || 0) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-orange">
|
|
|
|
|
+ <div class="icon-badge icon-orange"><i class="pi pi-chart-line"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总销售额</div>
|
|
|
|
|
+ <div class="stat-value text-orange-700">¥{{ formatAmount(memberData.totalSales || 0) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-indigo">
|
|
|
|
|
+ <div class="icon-badge icon-indigo"><i class="pi pi-users"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日日活</div>
|
|
|
|
|
+ <div class="stat-value text-indigo-700">{{ memberData.todayDAU || 0 }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-slate">
|
|
|
|
|
+ <div class="icon-badge icon-slate"><i class="pi pi-database"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总用户</div>
|
|
|
|
|
+ <div class="stat-value text-slate-800">{{ memberData.totalUsers || 0 }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-indigo">
|
|
|
|
|
+ <div class="icon-badge icon-indigo"><i class="pi pi-chart-bar"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日IP成交率</div>
|
|
|
|
|
+ <div class="stat-value text-indigo-700">{{ formatPercent(memberData.todayIpConversionRate) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-slate">
|
|
|
|
|
+ <div class="icon-badge icon-slate"><i class="pi pi-chart-line"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总IP成交率</div>
|
|
|
|
|
+ <div class="stat-value text-slate-800">{{ formatPercent(memberData.totalIpConversionRate) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <!-- 推广员视图 -->
|
|
|
|
|
- <div v-else-if="isPromoter" class="promoter-stats">
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>队员姓名</h3>
|
|
|
|
|
- <p class="stat-value">{{ memberStats?.name || '-' }}</p>
|
|
|
|
|
|
|
+ <!-- 团队用户视图 - 与TeamView保持一致 -->
|
|
|
|
|
+ <div v-if="isTeam && teamData" class="py-2">
|
|
|
|
|
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
|
|
+ <!-- 左侧:基本信息卡片(与右侧均分) -->
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <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="text-xl font-extrabold text-gray-900">我的团队信息</div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon="pi pi-palette"
|
|
|
|
|
+ label="选择主题"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ severity="secondary"
|
|
|
|
|
+ @click="openThemeDialog"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-y-5 gap-x-6">
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">团队名称</span>
|
|
|
|
|
+ <span class="text-base font-bold text-gray-900">{{ teamData.name }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">佣金比例</span>
|
|
|
|
|
+ <span class="text-base font-bold text-purple-700">{{ teamData.commissionRate }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <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="teamData.affCode"
|
|
|
|
|
+ @click="copyToClipboard(teamData.affCode)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ teamData.affCode }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex flex-col">
|
|
|
|
|
+ <span class="text-gray-500 text-sm mb-1">总用户数</span>
|
|
|
|
|
+ <span class="text-lg font-extrabold text-teal-600">{{ teamData.totalUsers || 0 }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="card">
|
|
|
|
|
- <h3>分成比例</h3>
|
|
|
|
|
- <p class="stat-value">{{ formatRate(memberStats?.commissionRate) }}%</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card revenue-card">
|
|
|
|
|
- <h3>总分成收入</h3>
|
|
|
|
|
- <p class="stat-value revenue-value">{{ formatAmount(memberStats?.totalCommission) }}</p>
|
|
|
|
|
- <p class="stat-label">累计分成</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="card revenue-card">
|
|
|
|
|
- <h3>今日分成收入</h3>
|
|
|
|
|
- <p class="stat-value revenue-value">{{ formatAmount(memberStats?.todayCommission) }}</p>
|
|
|
|
|
- <p class="stat-label">今日分成</p>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧:统计快照(与左侧均分) -->
|
|
|
|
|
+ <div class="grid grid-cols-2 gap-4">
|
|
|
|
|
+ <div class="stat-card stat-yellow">
|
|
|
|
|
+ <div class="icon-badge icon-yellow"><i class="pi pi-dollar"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日分润收入</div>
|
|
|
|
|
+ <div class="stat-value text-yellow-700">¥{{ formatAmount(teamData.todayRevenue) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-blue">
|
|
|
|
|
+ <div class="icon-badge icon-blue"><i class="pi pi-wallet"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总分润收入</div>
|
|
|
|
|
+ <div class="stat-value text-blue-700">¥{{ formatAmount(teamData.totalRevenue) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-green">
|
|
|
|
|
+ <div class="icon-badge icon-green"><i class="pi pi-shopping-cart"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日销售额</div>
|
|
|
|
|
+ <div class="stat-value text-green-700">¥{{ formatAmount(teamData.todaySales) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-orange">
|
|
|
|
|
+ <div class="icon-badge icon-orange"><i class="pi pi-chart-line"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总销售额</div>
|
|
|
|
|
+ <div class="stat-value text-orange-700">¥{{ formatAmount(teamData.totalSales) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-indigo">
|
|
|
|
|
+ <div class="icon-badge icon-indigo"><i class="pi pi-users"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日日活</div>
|
|
|
|
|
+ <div class="stat-value text-indigo-700">{{ teamData.todayDAU || 0 }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-slate">
|
|
|
|
|
+ <div class="icon-badge icon-slate"><i class="pi pi-database"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总用户</div>
|
|
|
|
|
+ <div class="stat-value text-slate-800">{{ teamData.totalUsers || 0 }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-indigo">
|
|
|
|
|
+ <div class="icon-badge icon-indigo"><i class="pi pi-chart-bar"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">今日IP成交率</div>
|
|
|
|
|
+ <div class="stat-value text-indigo-700">{{ formatPercent(ipStats?.todayIpConversionRate) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card stat-slate">
|
|
|
|
|
+ <div class="icon-badge icon-slate"><i class="pi pi-chart-line"></i></div>
|
|
|
|
|
+ <div class="stat-body">
|
|
|
|
|
+ <div class="stat-title">总IP成交率</div>
|
|
|
|
|
+ <div class="stat-value text-slate-800">{{ formatPercent(ipStats?.totalIpConversionRate) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -125,6 +266,57 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 主题选择弹窗 -->
|
|
|
|
|
+ <Dialog
|
|
|
|
|
+ v-model:visible="themeDialog"
|
|
|
|
|
+ :modal="true"
|
|
|
|
|
+ header="选择UI主题"
|
|
|
|
|
+ :style="{ width: '700px' }"
|
|
|
|
|
+ position="center"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="theme-selector">
|
|
|
|
|
+ <div class="theme-grid">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="theme in themeOptions"
|
|
|
|
|
+ :key="theme.value"
|
|
|
|
|
+ class="theme-card"
|
|
|
|
|
+ :class="{ 'theme-selected': selectedTheme === theme.value }"
|
|
|
|
|
+ @click="selectTheme(theme.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="theme-preview"
|
|
|
|
|
+ :data-theme="theme.value"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ '--preview-brand': theme.colors.brand,
|
|
|
|
|
+ '--preview-surface': theme.colors.surface,
|
|
|
|
|
+ '--preview-text': theme.colors.text
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="preview-header">
|
|
|
|
|
+ <div class="preview-brand-color" :style="{ backgroundColor: theme.colors.brand }"></div>
|
|
|
|
|
+ <span class="preview-title" :style="{ color: theme.colors.text }">示例标题</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="preview-body" :style="{ backgroundColor: theme.colors.surface }">
|
|
|
|
|
+ <div class="preview-content" :style="{ color: theme.colors.text }">
|
|
|
|
|
+ <div class="preview-text">这是预览文本</div>
|
|
|
|
|
+ <div class="preview-button" :style="{ backgroundColor: theme.colors.brand, color: '#fff' }">
|
|
|
|
|
+ 按钮
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="theme-name">{{ theme.label }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
|
|
+ <Button label="取消" severity="secondary" @click="themeDialog = false" />
|
|
|
|
|
+ <Button label="保存" severity="success" @click="saveTheme" :loading="themeLoading" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
<!-- 团队数据表格 (仅管理员可见) -->
|
|
<!-- 团队数据表格 (仅管理员可见) -->
|
|
|
<div v-if="isAdmin" class="teams-table-container">
|
|
<div v-if="isAdmin" class="teams-table-container">
|
|
|
<h2>团队数据一览</h2>
|
|
<h2>团队数据一览</h2>
|
|
@@ -170,14 +362,18 @@
|
|
|
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 } from '@/services/api'
|
|
|
|
|
|
|
+import { getAllTeamStatistics, getIncomeStatistics, listMembers, getTeamIpConversionRate, listTeams, getTeamDomainDailyStatistics, getTeamDomainAllStatistics, updateTeamThemeColor } from '@/services/api'
|
|
|
|
|
+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'
|
|
|
import DataTable from 'primevue/datatable'
|
|
import DataTable from 'primevue/datatable'
|
|
|
import Column from 'primevue/column'
|
|
import Column from 'primevue/column'
|
|
|
|
|
+import Button from 'primevue/button'
|
|
|
|
|
+import Dialog from 'primevue/dialog'
|
|
|
|
|
|
|
|
const userStore = useUserStore()
|
|
const userStore = useUserStore()
|
|
|
const teamStore = useTeamStore()
|
|
const teamStore = useTeamStore()
|
|
|
|
|
+const toast = useToast()
|
|
|
const role = computed(() => userStore.userInfo?.role)
|
|
const role = computed(() => userStore.userInfo?.role)
|
|
|
|
|
|
|
|
const isAdmin = inject('isAdmin')
|
|
const isAdmin = inject('isAdmin')
|
|
@@ -193,6 +389,185 @@ const loading = ref(true)
|
|
|
const teams = ref([])
|
|
const teams = ref([])
|
|
|
const chartInstance = ref(null)
|
|
const chartInstance = ref(null)
|
|
|
const selectedChartTeamId = ref(null) // 用于图表的团队选择
|
|
const selectedChartTeamId = ref(null) // 用于图表的团队选择
|
|
|
|
|
+const ipStats = ref(null) // IP成交率数据
|
|
|
|
|
+const teamListData = ref(null) // 团队列表数据(包含基础信息如affCode等)
|
|
|
|
|
+
|
|
|
|
|
+// 主题选择相关
|
|
|
|
|
+const themeDialog = ref(false)
|
|
|
|
|
+const themeLoading = ref(false)
|
|
|
|
|
+const selectedTheme = ref('')
|
|
|
|
|
+const themeOptions = [
|
|
|
|
|
+ {
|
|
|
|
|
+ value: '',
|
|
|
|
|
+ label: '默认',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#10b981',
|
|
|
|
|
+ surface: '#0f172a',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'dark',
|
|
|
|
|
+ label: '深色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#10b981',
|
|
|
|
|
+ surface: '#1e293b',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'light',
|
|
|
|
|
+ label: '浅色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#059669',
|
|
|
|
|
+ surface: '#f8fafc',
|
|
|
|
|
+ text: '#1e293b'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'black',
|
|
|
|
|
+ label: '黑色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#60a5fa',
|
|
|
|
|
+ surface: '#0a0a0a',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'purple',
|
|
|
|
|
+ label: '紫色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#a855f7',
|
|
|
|
|
+ surface: '#3b1f5e',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'orange',
|
|
|
|
|
+ label: '橙色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#fb923c',
|
|
|
|
|
+ surface: '#7c2d12',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'green',
|
|
|
|
|
+ label: '绿色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#22c55e',
|
|
|
|
|
+ surface: '#14532d',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'galaxy',
|
|
|
|
|
+ label: '星空',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#06b6d4',
|
|
|
|
|
+ surface: '#1e1b4b',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'rose',
|
|
|
|
|
+ label: '玫瑰',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#f43f5e',
|
|
|
|
|
+ surface: '#831843',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'blue',
|
|
|
|
|
+ label: '蓝色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#3b82f6',
|
|
|
|
|
+ surface: '#1e3a8a',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'red',
|
|
|
|
|
+ label: '红色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#f87171',
|
|
|
|
|
+ surface: '#7f1d1d',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'yellow',
|
|
|
|
|
+ label: '黄色',
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ brand: '#fbbf24',
|
|
|
|
|
+ surface: '#78350f',
|
|
|
|
|
+ text: '#f1f5f9'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const teamData = computed(() => {
|
|
|
|
|
+ if (isTeam.value) {
|
|
|
|
|
+ // 优先从listTeams返回的数据中获取基础信息(包含affCode等)
|
|
|
|
|
+ if (teamListData.value?.content && teamListData.value.content.length > 0) {
|
|
|
|
|
+ const baseTeamInfo = teamListData.value.content[0]
|
|
|
|
|
+ // 合并统计数据
|
|
|
|
|
+ if (teamStats.value) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...baseTeamInfo,
|
|
|
|
|
+ todayRevenue: teamStats.value.todayRevenue || baseTeamInfo.todayRevenue || 0,
|
|
|
|
|
+ totalRevenue: teamStats.value.totalRevenue || baseTeamInfo.totalRevenue || 0,
|
|
|
|
|
+ todaySales: teamStats.value.todaySales || baseTeamInfo.todaySales || 0,
|
|
|
|
|
+ totalSales: teamStats.value.totalSales || baseTeamInfo.totalSales || 0,
|
|
|
|
|
+ todayDAU: teamStats.value.todayDAU || baseTeamInfo.todayDAU || 0,
|
|
|
|
|
+ totalUsers: teamStats.value.totalUsers || baseTeamInfo.totalUsers || 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return baseTeamInfo
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果没有listTeams数据,从teamStats中构建(fallback)
|
|
|
|
|
+ if (teamStats.value) {
|
|
|
|
|
+ if (teamStats.value.allTeams && teamStats.value.allTeams.length > 0) {
|
|
|
|
|
+ return teamStats.value.allTeams[0]
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ name: teamStats.value.name || '-',
|
|
|
|
|
+ commissionRate: teamStats.value.averageCommissionRate || 0,
|
|
|
|
|
+ affCode: teamStats.value.affCode || '-',
|
|
|
|
|
+ totalUsers: teamStats.value.totalUsers || 0,
|
|
|
|
|
+ todayRevenue: teamStats.value.todayRevenue || 0,
|
|
|
|
|
+ totalRevenue: teamStats.value.totalRevenue || 0,
|
|
|
|
|
+ todaySales: teamStats.value.todaySales || 0,
|
|
|
|
|
+ totalSales: teamStats.value.totalSales || 0,
|
|
|
|
|
+ todayDAU: teamStats.value.todayDAU || 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 推广员数据计算属性
|
|
|
|
|
+const memberData = computed(() => {
|
|
|
|
|
+ if (isPromoter.value && memberStats.value) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ name: memberStats.value.name || '-',
|
|
|
|
|
+ commissionRate: memberStats.value.commissionRate || 0,
|
|
|
|
|
+ affCode: memberStats.value.affCode || null,
|
|
|
|
|
+ todayRevenue: memberStats.value.todayRevenue || memberStats.value.todayCommission || 0,
|
|
|
|
|
+ totalRevenue: memberStats.value.totalRevenue || memberStats.value.totalCommission || 0,
|
|
|
|
|
+ todayCommission: memberStats.value.todayCommission || 0,
|
|
|
|
|
+ totalCommission: memberStats.value.totalCommission || 0,
|
|
|
|
|
+ todaySales: memberStats.value.todaySales || 0,
|
|
|
|
|
+ totalSales: memberStats.value.totalSales || 0,
|
|
|
|
|
+ todayDAU: memberStats.value.todayDAU || 0,
|
|
|
|
|
+ totalUsers: memberStats.value.totalUsers || 0,
|
|
|
|
|
+ todayIpConversionRate: memberStats.value.todayIpConversionRate || null,
|
|
|
|
|
+ totalIpConversionRate: memberStats.value.totalIpConversionRate || null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
// 团队选项
|
|
// 团队选项
|
|
|
const teamOptions = computed(() => {
|
|
const teamOptions = computed(() => {
|
|
@@ -218,6 +593,53 @@ const formatRate = (rate) => {
|
|
|
return Number(rate).toFixed(2)
|
|
return Number(rate).toFixed(2)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 百分比格式化(0-1 -> 0.00%)
|
|
|
|
|
+const formatPercent = (rate) => {
|
|
|
|
|
+ if (rate === undefined || rate === null) return '0.00%'
|
|
|
|
|
+ const num = Number(rate)
|
|
|
|
|
+ if (Number.isNaN(num)) return '0.00%'
|
|
|
|
|
+ return `${(num * 100).toFixed(2)}%`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 复制到剪贴板
|
|
|
|
|
+const copyToClipboard = async (text) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(text)
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '已复制到剪贴板',
|
|
|
|
|
+ life: 2000
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ const textArea = document.createElement('textarea')
|
|
|
|
|
+ textArea.value = text
|
|
|
|
|
+ document.body.appendChild(textArea)
|
|
|
|
|
+ textArea.select()
|
|
|
|
|
+ document.execCommand('copy')
|
|
|
|
|
+ document.body.removeChild(textArea)
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '已复制到剪贴板',
|
|
|
|
|
+ life: 2000
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 加载IP成交率数据
|
|
|
|
|
+const loadIpConversionRate = async () => {
|
|
|
|
|
+ if (!isTeam.value) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await getTeamIpConversionRate()
|
|
|
|
|
+ ipStats.value = data
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // 团队角色无权限外的错误提示
|
|
|
|
|
+ // 接口做了权限限制,这里仅在请求失败时给出轻提示
|
|
|
|
|
+ console.warn('获取IP成交率失败:', e)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 获取当前日期和7天前的日期
|
|
// 获取当前日期和7天前的日期
|
|
|
const getDateRange = () => {
|
|
const getDateRange = () => {
|
|
|
const endDate = new Date()
|
|
const endDate = new Date()
|
|
@@ -238,6 +660,53 @@ const formatDate = (date) => {
|
|
|
return `${year}-${month}-${day}`
|
|
return `${year}-${month}-${day}`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 打开主题选择弹窗
|
|
|
|
|
+const openThemeDialog = () => {
|
|
|
|
|
+ selectedTheme.value = teamData.value?.themeColor || ''
|
|
|
|
|
+ themeDialog.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 选择主题
|
|
|
|
|
+const selectTheme = (themeValue) => {
|
|
|
|
|
+ selectedTheme.value = themeValue
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存主题
|
|
|
|
|
+const saveTheme = async () => {
|
|
|
|
|
+ if (!teamData.value?.id) {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '团队信息不存在',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ themeLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await updateTeamThemeColor(selectedTheme.value)
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'success',
|
|
|
|
|
+ summary: '成功',
|
|
|
|
|
+ detail: '主题保存成功',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ themeDialog.value = false
|
|
|
|
|
+ // 刷新数据以获取最新的themeColor
|
|
|
|
|
+ await loadTeamStats()
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.add({
|
|
|
|
|
+ severity: 'error',
|
|
|
|
|
+ summary: '错误',
|
|
|
|
|
+ detail: '主题保存失败',
|
|
|
|
|
+ life: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ themeLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 加载团队统计数据
|
|
// 加载团队统计数据
|
|
|
const loadTeamStats = async () => {
|
|
const loadTeamStats = async () => {
|
|
|
try {
|
|
try {
|
|
@@ -251,28 +720,65 @@ const loadTeamStats = async () => {
|
|
|
selectedTeamId.value = teams.value[0].id
|
|
selectedTeamId.value = teams.value[0].id
|
|
|
}
|
|
}
|
|
|
} else if (isTeam.value) {
|
|
} else if (isTeam.value) {
|
|
|
- const data = await getAllTeamStatistics()
|
|
|
|
|
|
|
+ // 团队用户需要同时调用listTeams获取基础信息和getAllTeamStatistics获取统计数据
|
|
|
|
|
+ const [teamsResponse, statsData] = await Promise.all([
|
|
|
|
|
+ listTeams(0, 20),
|
|
|
|
|
+ getAllTeamStatistics()
|
|
|
|
|
+ ])
|
|
|
|
|
+ teamListData.value = teamsResponse
|
|
|
// 为 team 角色添加团队名称
|
|
// 为 team 角色添加团队名称
|
|
|
- if (data.allTeams && data.allTeams.length > 0) {
|
|
|
|
|
- data.name = data.allTeams[0].name
|
|
|
|
|
|
|
+ if (statsData.allTeams && statsData.allTeams.length > 0) {
|
|
|
|
|
+ statsData.name = statsData.allTeams[0].name
|
|
|
}
|
|
}
|
|
|
- teamStats.value = data
|
|
|
|
|
|
|
+ teamStats.value = statsData
|
|
|
} else if (isPromoter.value) {
|
|
} else if (isPromoter.value) {
|
|
|
- // 推广员角色加载个人统计数据,使用团队成员列表接口
|
|
|
|
|
|
|
+ // 推广员角色加载个人统计数据,使用团队成员列表接口和域名统计接口
|
|
|
const currentUserId = userStore.userInfo?.id
|
|
const currentUserId = userStore.userInfo?.id
|
|
|
if (currentUserId) {
|
|
if (currentUserId) {
|
|
|
- const response = await listMembers(0, 1, undefined, undefined, currentUserId)
|
|
|
|
|
|
|
+ // 并行请求团队成员数据、域名统计数据和IP成交率数据
|
|
|
|
|
+ const promises = [
|
|
|
|
|
+ listMembers(0, 1, undefined, undefined, currentUserId),
|
|
|
|
|
+ getTeamDomainDailyStatistics(),
|
|
|
|
|
+ getTeamDomainAllStatistics(),
|
|
|
|
|
+ getTeamIpConversionRate().catch(() => null) // 如果接口不支持推广员,返回null
|
|
|
|
|
+ ]
|
|
|
|
|
+ const [membersResponse, dailyStatsResponse, allStatsResponse, ipStatsData] = await Promise.all(promises)
|
|
|
|
|
+
|
|
|
// 从列表接口返回的数据中提取第一个成员(当前用户)的统计数据
|
|
// 从列表接口返回的数据中提取第一个成员(当前用户)的统计数据
|
|
|
- if (response.content && response.content.length > 0) {
|
|
|
|
|
- const memberData = response.content[0]
|
|
|
|
|
- memberStats.value = {
|
|
|
|
|
- name: memberData.name || '-',
|
|
|
|
|
- commissionRate: memberData.commissionRate || 0,
|
|
|
|
|
- totalCommission: memberData.totalRevenue || 0,
|
|
|
|
|
- totalOrders: 0, // 列表接口没有返回订单数,设为0
|
|
|
|
|
- todayCommission: memberData.todayRevenue || 0,
|
|
|
|
|
- todayOrders: 0 // 列表接口没有返回订单数,设为0
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ let memberDataItem = null
|
|
|
|
|
+ if (membersResponse.content && membersResponse.content.length > 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 = {
|
|
|
|
|
+ name: memberDataItem?.name || '-',
|
|
|
|
|
+ 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成交率数据
|
|
|
|
|
+ todayIpConversionRate: ipStatsData?.todayIpConversionRate || null,
|
|
|
|
|
+ totalIpConversionRate: ipStatsData?.totalIpConversionRate || null,
|
|
|
|
|
+ totalOrders: 0, // 列表接口没有返回订单数,设为0
|
|
|
|
|
+ todayOrders: 0 // 列表接口没有返回订单数,设为0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存IP成交率数据到ipStats,供模板使用
|
|
|
|
|
+ if (ipStatsData) {
|
|
|
|
|
+ ipStats.value = ipStatsData
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -493,6 +999,11 @@ onMounted(async () => {
|
|
|
}
|
|
}
|
|
|
await loadTeamStats()
|
|
await loadTeamStats()
|
|
|
|
|
|
|
|
|
|
+ // 团队用户加载IP成交率
|
|
|
|
|
+ if (isTeam.value) {
|
|
|
|
|
+ await loadIpConversionRate()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 延迟加载图表数据,确保DOM已渲染
|
|
// 延迟加载图表数据,确保DOM已渲染
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
loadIncomeStats()
|
|
loadIncomeStats()
|
|
@@ -729,4 +1240,302 @@ onUnmounted(() => {
|
|
|
color: #8b5cf6;
|
|
color: #8b5cf6;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 团队用户视图样式 - 与TeamView保持一致 */
|
|
|
|
|
+.py-2 {
|
|
|
|
|
+ padding-top: 0.5rem;
|
|
|
|
|
+ padding-bottom: 0.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.grid-cols-1 {
|
|
|
|
|
+ grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.grid-cols-2 {
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (min-width: 1024px) {
|
|
|
|
|
+ .lg\:grid-cols-2 {
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (min-width: 640px) {
|
|
|
|
|
+ .sm\:grid-cols-2 {
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.gap-4 {
|
|
|
|
|
+ gap: 1rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.gap-x-6 {
|
|
|
|
|
+ column-gap: 1.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.gap-y-5 {
|
|
|
|
|
+ row-gap: 1.25rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rounded-lg {
|
|
|
|
|
+ border-radius: 0.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.border {
|
|
|
|
|
+ border-width: 1px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.border-gray-200 {
|
|
|
|
|
+ border-color: #e5e7eb;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bg-white {
|
|
|
|
|
+ background-color: white;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.p-6 {
|
|
|
|
|
+ padding: 1.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shadow-sm {
|
|
|
|
|
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-xl {
|
|
|
|
|
+ font-size: 1.25rem;
|
|
|
|
|
+ line-height: 1.75rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.font-extrabold {
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mb-4 {
|
|
|
|
|
+ margin-bottom: 1rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-gray-900 {
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-gray-500 {
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-sm {
|
|
|
|
|
+ font-size: 0.875rem;
|
|
|
|
|
+ line-height: 1.25rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mb-1 {
|
|
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-base {
|
|
|
|
|
+ font-size: 1rem;
|
|
|
|
|
+ line-height: 1.5rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.font-bold {
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-purple-700 {
|
|
|
|
|
+ color: #7c3aed;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-orange-600 {
|
|
|
|
|
+ color: #ea580c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.font-mono {
|
|
|
|
|
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cursor-pointer {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-lg {
|
|
|
|
|
+ font-size: 1.125rem;
|
|
|
|
|
+ line-height: 1.75rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-teal-600 {
|
|
|
|
|
+ color: #0d9488;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.flex {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.flex-col {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.copyable-text {
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.copyable-text:hover {
|
|
|
|
|
+ background-color: #e5e7eb;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.copyable-text:active {
|
|
|
|
|
+ background-color: #d1d5db;
|
|
|
|
|
+ transform: scale(0.98);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 统计卡片样式 */
|
|
|
|
|
+.stat-card {
|
|
|
|
|
+ border-radius: 0.5rem;
|
|
|
|
|
+ border: 1px solid #fde68a; /* amber-300 */
|
|
|
|
|
+ padding: 1rem;
|
|
|
|
|
+ background: #fffbeb; /* amber-50 统一浅黄色背景 */
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 1rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-title {
|
|
|
|
|
+ font-size: 0.875rem; /* text-sm */
|
|
|
|
|
+ color: #4b5563; /* gray-600 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-value {
|
|
|
|
|
+ margin-top: 0.25rem;
|
|
|
|
|
+ font-size: 1.5rem; /* text-2xl */
|
|
|
|
|
+ font-weight: 800; /* extrabold */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.icon-badge {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border-radius: 9999px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 1.125rem; /* text-lg */
|
|
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.icon-yellow { background: #fef3c7; border: 1px solid #fde68a; color: #ca8a04; }
|
|
|
|
|
+.icon-blue { background: #dbeafe; border: 1px solid #bfdbfe; color: #2563eb; }
|
|
|
|
|
+.icon-green { background: #dcfce7; border: 1px solid #bbf7d0; color: #16a34a; }
|
|
|
|
|
+.icon-orange { background: #ffedd5; border: 1px solid #fed7aa; color: #ea580c; }
|
|
|
|
|
+.icon-indigo { background: #e0e7ff; border: 1px solid #c7d2fe; color: #4f46e5; }
|
|
|
|
|
+.icon-slate { background: #f1f5f9; border: 1px solid #e2e8f0; color: #475569; }
|
|
|
|
|
+
|
|
|
|
|
+.stat-yellow { border-color: #fde68a; background: #fffbeb; }
|
|
|
|
|
+.stat-blue { border-color: #bfdbfe; background: #eff6ff; }
|
|
|
|
|
+.stat-green { border-color: #bbf7d0; background: #f0fdf4; }
|
|
|
|
|
+.stat-orange { border-color: #fed7aa; background: #fff7ed; }
|
|
|
|
|
+.stat-indigo { border-color: #c7d2fe; background: #eef2ff; }
|
|
|
|
|
+.stat-slate { border-color: #e2e8f0; background: #f8fafc; }
|
|
|
|
|
+
|
|
|
|
|
+.text-yellow-700 { color: #a16207; }
|
|
|
|
|
+.text-blue-700 { color: #1d4ed8; }
|
|
|
|
|
+.text-green-700 { color: #15803d; }
|
|
|
|
|
+.text-orange-700 { color: #c2410c; }
|
|
|
|
|
+.text-indigo-700 { color: #4338ca; }
|
|
|
|
|
+.text-slate-800 { color: #1e293b; }
|
|
|
|
|
+/* 主题选择器样式 */
|
|
|
|
|
+.theme-selector {
|
|
|
|
|
+ padding: 1rem 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.theme-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
|
|
|
+ gap: 1rem;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.theme-card {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 0.5rem;
|
|
|
|
|
+ border: 2px solid transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.theme-card:hover {
|
|
|
|
|
+ background-color: #f3f4f6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.theme-selected {
|
|
|
|
|
+ border-color: #3b82f6;
|
|
|
|
|
+ background-color: #eff6ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.theme-preview {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 120px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-header {
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ background-color: var(--preview-surface);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-brand-color {
|
|
|
|
|
+ width: 12px;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-title {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-body {
|
|
|
|
|
+ height: calc(100% - 30px);
|
|
|
|
|
+ padding: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-text {
|
|
|
|
|
+ font-size: 9px;
|
|
|
|
|
+ opacity: 0.8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.preview-button {
|
|
|
|
|
+ width: fit-content;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 8px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.theme-name {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 0.875rem;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #374151;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|