mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 公开统计功能增强 - 独立设置栏目和双Y轴折线图
- 将公开统计设置从品牌设置移至独立栏目 - 用三合一双Y轴折线图替代条形图(Chart.js + vue-chartjs) - 左Y轴显示Tokens,右Y轴显示活跃数量 - 添加暂无数据状态的友好提示 - 修复Y轴可能显示负数的问题(设置min:0) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
11
web/admin-spa/package-lock.json
generated
11
web/admin-spa/package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"element-plus": "^2.4.4",
|
"element-plus": "^2.4.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"xlsx-js-style": "^1.2.0"
|
"xlsx-js-style": "^1.2.0"
|
||||||
@@ -5131,6 +5132,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chartjs": {
|
||||||
|
"version": "5.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
|
||||||
|
"integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-demi": {
|
"node_modules/vue-demi": {
|
||||||
"version": "0.14.10",
|
"version": "0.14.10",
|
||||||
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
|
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"element-plus": "^2.4.4",
|
"element-plus": "^2.4.4",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"xlsx-js-style": "^1.2.0"
|
"xlsx-js-style": "^1.2.0"
|
||||||
|
|||||||
@@ -68,78 +68,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Token使用趋势 -->
|
<!-- 趋势图表(三合一双Y轴折线图) -->
|
||||||
<div
|
<div v-if="hasAnyTrendData" class="mt-4">
|
||||||
v-if="
|
<div class="section-title">使用趋势(近7天)</div>
|
||||||
authStore.publicStats.showOptions?.tokenTrends && authStore.publicStats.tokenTrends?.length
|
<div class="chart-container">
|
||||||
"
|
<Line :data="chartData" :options="chartOptions" />
|
||||||
class="mt-4"
|
</div>
|
||||||
>
|
<!-- 图例 -->
|
||||||
<div class="section-title">Token 使用趋势(近7天)</div>
|
<div class="chart-legend">
|
||||||
<div class="trend-chart">
|
<div v-if="authStore.publicStats.showOptions?.tokenTrends" class="legend-item">
|
||||||
<div
|
<span class="legend-dot legend-tokens"></span>
|
||||||
v-for="(item, index) in authStore.publicStats.tokenTrends"
|
<span class="legend-text">Tokens</span>
|
||||||
:key="index"
|
</div>
|
||||||
class="trend-bar-wrapper"
|
<div v-if="authStore.publicStats.showOptions?.apiKeysTrends" class="legend-item">
|
||||||
>
|
<span class="legend-dot legend-keys"></span>
|
||||||
<div
|
<span class="legend-text">活跃 Keys</span>
|
||||||
class="trend-bar trend-bar-tokens"
|
</div>
|
||||||
:style="{ height: `${getTrendBarHeight(item.tokens, 'tokens')}%` }"
|
<div v-if="authStore.publicStats.showOptions?.accountTrends" class="legend-item">
|
||||||
:title="`${formatDate(item.date)}: ${formatTokens(item.tokens)} tokens`"
|
<span class="legend-dot legend-accounts"></span>
|
||||||
></div>
|
<span class="legend-text">活跃账号</span>
|
||||||
<div class="trend-label">{{ formatDateShort(item.date) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Keys 使用趋势 -->
|
<!-- 暂无趋势数据 -->
|
||||||
<div
|
<div v-else-if="hasTrendOptionsEnabled" class="empty-state mt-4">
|
||||||
v-if="
|
<i class="fas fa-chart-line empty-icon"></i>
|
||||||
authStore.publicStats.showOptions?.apiKeysTrends &&
|
<p class="empty-text">暂无趋势数据</p>
|
||||||
authStore.publicStats.apiKeysTrends?.length
|
<p class="empty-hint">数据将在有请求后自动更新</p>
|
||||||
"
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
<div class="section-title">API Keys 活跃趋势(近7天)</div>
|
|
||||||
<div class="trend-chart">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in authStore.publicStats.apiKeysTrends"
|
|
||||||
:key="index"
|
|
||||||
class="trend-bar-wrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="trend-bar trend-bar-keys"
|
|
||||||
:style="{ height: `${getTrendBarHeight(item.activeKeys, 'apiKeys')}%` }"
|
|
||||||
:title="`${formatDate(item.date)}: ${item.activeKeys} 个活跃 Key`"
|
|
||||||
></div>
|
|
||||||
<div class="trend-label">{{ formatDateShort(item.date) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 账号使用趋势 -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
authStore.publicStats.showOptions?.accountTrends &&
|
|
||||||
authStore.publicStats.accountTrends?.length
|
|
||||||
"
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
<div class="section-title">账号活跃趋势(近7天)</div>
|
|
||||||
<div class="trend-chart">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in authStore.publicStats.accountTrends"
|
|
||||||
:key="index"
|
|
||||||
class="trend-bar-wrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="trend-bar trend-bar-accounts"
|
|
||||||
:style="{ height: `${getTrendBarHeight(item.activeAccounts, 'accounts')}%` }"
|
|
||||||
:title="`${formatDate(item.date)}: ${item.activeAccounts} 个活跃账号`"
|
|
||||||
></div>
|
|
||||||
<div class="trend-label">{{ formatDateShort(item.date) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,14 +103,238 @@
|
|||||||
<div v-else-if="authStore.publicStatsLoading" class="public-stats-loading">
|
<div v-else-if="authStore.publicStatsLoading" class="public-stats-loading">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 无数据状态 -->
|
||||||
|
<div v-else class="public-stats-empty">
|
||||||
|
<i class="fas fa-chart-pie empty-icon"></i>
|
||||||
|
<p class="empty-text">暂无统计数据</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { Line } from 'vue-chartjs'
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler
|
||||||
|
} from 'chart.js'
|
||||||
|
|
||||||
|
// 注册 Chart.js 组件
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler
|
||||||
|
)
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 检查是否有任何趋势选项启用
|
||||||
|
const hasTrendOptionsEnabled = computed(() => {
|
||||||
|
const opts = authStore.publicStats?.showOptions
|
||||||
|
return opts?.tokenTrends || opts?.apiKeysTrends || opts?.accountTrends
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查是否有实际趋势数据
|
||||||
|
const hasAnyTrendData = computed(() => {
|
||||||
|
const stats = authStore.publicStats
|
||||||
|
if (!stats) return false
|
||||||
|
|
||||||
|
const opts = stats.showOptions || {}
|
||||||
|
const hasTokens = opts.tokenTrends && stats.tokenTrends?.length > 0
|
||||||
|
const hasKeys = opts.apiKeysTrends && stats.apiKeysTrends?.length > 0
|
||||||
|
const hasAccounts = opts.accountTrends && stats.accountTrends?.length > 0
|
||||||
|
|
||||||
|
return hasTokens || hasKeys || hasAccounts
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图表数据
|
||||||
|
const chartData = computed(() => {
|
||||||
|
const stats = authStore.publicStats
|
||||||
|
if (!stats) return { labels: [], datasets: [] }
|
||||||
|
|
||||||
|
const opts = stats.showOptions || {}
|
||||||
|
|
||||||
|
// 获取日期标签(优先使用 tokenTrends)
|
||||||
|
const labels =
|
||||||
|
stats.tokenTrends?.map((t) => formatDateShort(t.date)) ||
|
||||||
|
stats.apiKeysTrends?.map((t) => formatDateShort(t.date)) ||
|
||||||
|
stats.accountTrends?.map((t) => formatDateShort(t.date)) ||
|
||||||
|
[]
|
||||||
|
|
||||||
|
const datasets = []
|
||||||
|
|
||||||
|
// Token 趋势(左Y轴)
|
||||||
|
if (opts.tokenTrends && stats.tokenTrends?.length > 0) {
|
||||||
|
datasets.push({
|
||||||
|
label: 'Tokens',
|
||||||
|
data: stats.tokenTrends.map((t) => t.tokens),
|
||||||
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
yAxisID: 'y',
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 5
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Keys 趋势(右Y轴)
|
||||||
|
if (opts.apiKeysTrends && stats.apiKeysTrends?.length > 0) {
|
||||||
|
datasets.push({
|
||||||
|
label: '活跃 Keys',
|
||||||
|
data: stats.apiKeysTrends.map((t) => t.activeKeys),
|
||||||
|
borderColor: 'rgb(34, 197, 94)',
|
||||||
|
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||||
|
yAxisID: 'y1',
|
||||||
|
tension: 0.3,
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 5
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账号趋势(右Y轴)
|
||||||
|
if (opts.accountTrends && stats.accountTrends?.length > 0) {
|
||||||
|
datasets.push({
|
||||||
|
label: '活跃账号',
|
||||||
|
data: stats.accountTrends.map((t) => t.activeAccounts),
|
||||||
|
borderColor: 'rgb(168, 85, 247)',
|
||||||
|
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
||||||
|
yAxisID: 'y1',
|
||||||
|
tension: 0.3,
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 3,
|
||||||
|
pointHoverRadius: 5
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { labels, datasets }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图表配置
|
||||||
|
const chartOptions = computed(() => {
|
||||||
|
const isDark = document.documentElement.classList.contains('dark')
|
||||||
|
const textColor = isDark ? 'rgba(156, 163, 175, 1)' : 'rgba(107, 114, 128, 1)'
|
||||||
|
const gridColor = isDark ? 'rgba(75, 85, 99, 0.3)' : 'rgba(229, 231, 235, 0.8)'
|
||||||
|
|
||||||
|
return {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: isDark ? 'rgba(31, 41, 55, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
||||||
|
titleColor: isDark ? '#e5e7eb' : '#1f2937',
|
||||||
|
bodyColor: isDark ? '#d1d5db' : '#4b5563',
|
||||||
|
borderColor: isDark ? 'rgba(75, 85, 99, 0.5)' : 'rgba(229, 231, 235, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 10,
|
||||||
|
displayColors: true,
|
||||||
|
callbacks: {
|
||||||
|
label: function (context) {
|
||||||
|
let label = context.dataset.label || ''
|
||||||
|
if (label) {
|
||||||
|
label += ': '
|
||||||
|
}
|
||||||
|
if (context.dataset.yAxisID === 'y') {
|
||||||
|
label += formatTokens(context.parsed.y)
|
||||||
|
} else {
|
||||||
|
label += context.parsed.y
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
color: gridColor,
|
||||||
|
drawBorder: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColor,
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
min: 0,
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Tokens',
|
||||||
|
color: 'rgb(59, 130, 246)',
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: gridColor,
|
||||||
|
drawBorder: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColor,
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
},
|
||||||
|
callback: function (value) {
|
||||||
|
return formatTokensShort(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
min: 0,
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: '数量',
|
||||||
|
color: 'rgb(34, 197, 94)',
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: textColor,
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
},
|
||||||
|
stepSize: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 格式化运行时间
|
// 格式化运行时间
|
||||||
function formatUptime(seconds) {
|
function formatUptime(seconds) {
|
||||||
const days = Math.floor(seconds / 86400)
|
const days = Math.floor(seconds / 86400)
|
||||||
@@ -192,6 +372,18 @@ function formatTokens(tokens) {
|
|||||||
return tokens.toString()
|
return tokens.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化 tokens(简短版,用于Y轴)
|
||||||
|
function formatTokensShort(tokens) {
|
||||||
|
if (tokens >= 1000000000) {
|
||||||
|
return (tokens / 1000000000).toFixed(0) + 'B'
|
||||||
|
} else if (tokens >= 1000000) {
|
||||||
|
return (tokens / 1000000).toFixed(0) + 'M'
|
||||||
|
} else if (tokens >= 1000) {
|
||||||
|
return (tokens / 1000).toFixed(0) + 'K'
|
||||||
|
}
|
||||||
|
return tokens.toString()
|
||||||
|
}
|
||||||
|
|
||||||
// 获取平台图标
|
// 获取平台图标
|
||||||
function getPlatformIcon(platform) {
|
function getPlatformIcon(platform) {
|
||||||
const icons = {
|
const icons = {
|
||||||
@@ -225,16 +417,6 @@ function formatModelName(model) {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期
|
|
||||||
function formatDate(dateStr) {
|
|
||||||
if (!dateStr) return ''
|
|
||||||
const parts = dateStr.split('-')
|
|
||||||
if (parts.length === 3) {
|
|
||||||
return `${parts[1]}月${parts[2]}日`
|
|
||||||
}
|
|
||||||
return dateStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化日期(短格式)
|
// 格式化日期(短格式)
|
||||||
function formatDateShort(dateStr) {
|
function formatDateShort(dateStr) {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
@@ -244,24 +426,6 @@ function formatDateShort(dateStr) {
|
|||||||
}
|
}
|
||||||
return dateStr
|
return dateStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算趋势图柱高度
|
|
||||||
const maxValues = computed(() => {
|
|
||||||
const stats = authStore.publicStats
|
|
||||||
if (!stats) return { tokens: 1, apiKeys: 1, accounts: 1 }
|
|
||||||
|
|
||||||
return {
|
|
||||||
tokens: Math.max(...(stats.tokenTrends?.map((t) => t.tokens) || [1]), 1),
|
|
||||||
apiKeys: Math.max(...(stats.apiKeysTrends?.map((t) => t.activeKeys) || [1]), 1),
|
|
||||||
accounts: Math.max(...(stats.accountTrends?.map((t) => t.activeAccounts) || [1]), 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function getTrendBarHeight(value, type) {
|
|
||||||
const max = maxValues.value[type] || 1
|
|
||||||
const height = (value / max) * 100
|
|
||||||
return Math.max(height, 5) // 最小高度5%
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -379,34 +543,56 @@ function getTrendBarHeight(value, type) {
|
|||||||
@apply w-10 text-right text-gray-500 dark:text-gray-400;
|
@apply w-10 text-right text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 趋势图表 */
|
/* 图表容器 */
|
||||||
.trend-chart {
|
.chart-container {
|
||||||
@apply flex h-24 items-end justify-between gap-1 rounded-lg bg-gray-50 p-2 dark:bg-gray-700/50;
|
@apply rounded-lg bg-gray-50 p-3 dark:bg-gray-700/50;
|
||||||
|
height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-bar-wrapper {
|
/* 图例 */
|
||||||
@apply flex flex-1 flex-col items-center;
|
.chart-legend {
|
||||||
|
@apply mt-2 flex flex-wrap items-center justify-center gap-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-bar {
|
.legend-item {
|
||||||
@apply w-full max-w-8 rounded-t transition-all duration-300;
|
@apply flex items-center gap-1.5;
|
||||||
min-height: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-bar-tokens {
|
.legend-dot {
|
||||||
@apply bg-gradient-to-t from-blue-500 to-blue-400;
|
@apply inline-block h-2.5 w-2.5 rounded-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-bar-keys {
|
.legend-tokens {
|
||||||
@apply bg-gradient-to-t from-green-500 to-green-400;
|
@apply bg-blue-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-bar-accounts {
|
.legend-keys {
|
||||||
@apply bg-gradient-to-t from-purple-500 to-purple-400;
|
@apply bg-green-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-label {
|
.legend-accounts {
|
||||||
@apply mt-1 text-center text-[10px] text-gray-500 dark:text-gray-400;
|
@apply bg-purple-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
@apply text-xs text-gray-600 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
@apply flex flex-col items-center justify-center rounded-lg bg-gray-50 py-6 dark:bg-gray-700/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
@apply mb-2 text-2xl text-gray-400 dark:text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
@apply text-sm text-gray-500 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
@apply mt-1 text-xs text-gray-400 dark:text-gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载状态 */
|
/* 加载状态 */
|
||||||
@@ -414,6 +600,10 @@ function getTrendBarHeight(value, type) {
|
|||||||
@apply flex items-center justify-center py-8;
|
@apply flex items-center justify-center py-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.public-stats-empty {
|
||||||
|
@apply flex flex-col items-center justify-center rounded-xl border border-gray-200/50 bg-white/80 py-8 backdrop-blur-sm dark:border-gray-700/50 dark:bg-gray-800/80;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
@apply h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent;
|
@apply h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,18 @@
|
|||||||
<i class="fas fa-robot mr-2"></i>
|
<i class="fas fa-robot mr-2"></i>
|
||||||
Claude 转发
|
Claude 转发
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'border-b-2 pb-2 text-sm font-medium transition-colors',
|
||||||
|
activeSection === 'publicStats'
|
||||||
|
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||||
|
]"
|
||||||
|
@click="activeSection = 'publicStats'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-line mr-2"></i>
|
||||||
|
公开统计
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -194,92 +206,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- 公开统计概览 -->
|
|
||||||
<tr class="table-row">
|
|
||||||
<td class="w-48 whitespace-nowrap px-6 py-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div
|
|
||||||
class="mr-3 flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-green-500 to-emerald-600"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chart-bar text-xs text-white" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
公开统计
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">登录页展示</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<label class="inline-flex cursor-pointer items-center">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsEnabled"
|
|
||||||
class="peer sr-only"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-green-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-green-800"
|
|
||||||
></div>
|
|
||||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
|
|
||||||
oemSettings.publicStatsEnabled ? '显示统计概览' : '隐藏统计概览'
|
|
||||||
}}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
启用后,未登录用户可在首页看到服务状态、模型使用分布等概览信息
|
|
||||||
</p>
|
|
||||||
<!-- 公开统计显示选项 -->
|
|
||||||
<div
|
|
||||||
v-if="oemSettings.publicStatsEnabled"
|
|
||||||
class="mt-4 space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-700/50"
|
|
||||||
>
|
|
||||||
<p class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
选择要公开显示的数据:
|
|
||||||
</p>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowModelDistribution"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">模型使用分布</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowTokenTrends"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>Token 使用趋势(近7天)</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowApiKeysTrends"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>API Keys 活跃趋势(近7天)</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowAccountTrends"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>账号活跃趋势(近7天)</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-6" colspan="2">
|
<td class="px-6 py-6" colspan="2">
|
||||||
@@ -432,86 +358,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 公开统计卡片 -->
|
|
||||||
<div class="glass-card p-4">
|
|
||||||
<div class="mb-3 flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md"
|
|
||||||
>
|
|
||||||
<i class="fas fa-chart-bar"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">公开统计</h3>
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">在登录页展示统计概览</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="inline-flex cursor-pointer items-center">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsEnabled"
|
|
||||||
class="peer sr-only"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-green-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-green-800"
|
|
||||||
></div>
|
|
||||||
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
|
|
||||||
oemSettings.publicStatsEnabled ? '显示统计概览' : '隐藏统计概览'
|
|
||||||
}}</span>
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
启用后,未登录用户可在首页看到服务状态、模型使用分布等概览信息
|
|
||||||
</p>
|
|
||||||
<!-- 公开统计显示选项 (移动端) -->
|
|
||||||
<div
|
|
||||||
v-if="oemSettings.publicStatsEnabled"
|
|
||||||
class="mt-4 space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-700/50"
|
|
||||||
>
|
|
||||||
<p class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
选择要公开显示的数据:
|
|
||||||
</p>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowModelDistribution"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">模型使用分布</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowTokenTrends"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>Token 使用趋势(近7天)</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowApiKeysTrends"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>API Keys 活跃趋势(近7天)</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
v-model="oemSettings.publicStatsShowAccountTrends"
|
|
||||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
|
||||||
>账号活跃趋势(近7天)</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 操作按钮卡片 -->
|
<!-- 操作按钮卡片 -->
|
||||||
<div class="glass-card p-4">
|
<div class="glass-card p-4">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
@@ -1191,6 +1037,137 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 公开统计设置部分 -->
|
||||||
|
<div v-show="activeSection === 'publicStats'">
|
||||||
|
<div class="rounded-lg bg-white/80 p-6 shadow-lg backdrop-blur-sm dark:bg-gray-800/80">
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-green-500 to-emerald-600 text-white shadow-md"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200">
|
||||||
|
公开统计概览
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
配置未登录用户可见的统计数据
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="inline-flex cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
v-model="oemSettings.publicStatsEnabled"
|
||||||
|
class="peer sr-only"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-green-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-green-800"
|
||||||
|
></div>
|
||||||
|
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{
|
||||||
|
oemSettings.publicStatsEnabled ? '已启用' : '已禁用'
|
||||||
|
}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据显示选项 -->
|
||||||
|
<div
|
||||||
|
v-if="oemSettings.publicStatsEnabled"
|
||||||
|
class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-700/50"
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
<i class="fas fa-eye mr-2 text-gray-400"></i>
|
||||||
|
选择要公开显示的数据:
|
||||||
|
</p>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="oemSettings.publicStatsShowModelDistribution"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>模型使用分布</span
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">显示各模型的使用占比</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="oemSettings.publicStatsShowTokenTrends"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>Token 使用趋势</span
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">显示近7天的Token使用量</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="oemSettings.publicStatsShowApiKeysTrends"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>API Keys 活跃趋势</span
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
显示近7天的活跃API Key数量
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="flex cursor-pointer items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="oemSettings.publicStatsShowAccountTrends"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>账号活跃趋势</span
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">显示近7天的活跃账号数量</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="mt-6 flex items-center justify-between">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary px-6 py-3"
|
||||||
|
:class="{ 'cursor-not-allowed opacity-50': saving }"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="saveOemSettings"
|
||||||
|
>
|
||||||
|
<div v-if="saving" class="loading-spinner mr-2"></div>
|
||||||
|
<i v-else class="fas fa-save mr-2" />
|
||||||
|
{{ saving ? '保存中...' : '保存设置' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="oemSettings.updatedAt" class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="fas fa-clock mr-1" />
|
||||||
|
最后更新:{{ formatDateTime(oemSettings.updatedAt) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user