feat: 迁移自定义功能至 1.5.9.9 版本
包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
@@ -5,21 +5,19 @@
|
||||
<div
|
||||
v-for="(stat, i) in statCards"
|
||||
:key="i"
|
||||
class="bg-bg-secondary-40 border border-border-secondary rounded-2xl p-6 relative overflow-hidden group hover:border-border-tertiary transition-all shadow-lg shadow-[0_10px_15px_var(--shadow-color)]"
|
||||
class="bg-zinc-900/40 border border-zinc-800 rounded-2xl p-6 relative overflow-hidden group hover:border-zinc-700 transition-all shadow-lg shadow-black/20"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div
|
||||
:class="[
|
||||
'p-3 rounded-xl border flex items-center justify-center',
|
||||
'p-3 rounded-xl border',
|
||||
stat.color === 'blue'
|
||||
? 'bg-primary-10 text-primary border-primary-20'
|
||||
? 'bg-blue-500/10 text-blue-500 border-blue-500/20'
|
||||
: stat.color === 'emerald'
|
||||
? 'bg-success-10 text-success border-success-20'
|
||||
? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20'
|
||||
: stat.color === 'pink'
|
||||
? 'bg-color-pink-alpha-10 text-color-pink border-color-pink-alpha-10'
|
||||
: stat.color === 'warning'
|
||||
? 'bg-warning-10 text-warning border-warning-20'
|
||||
: 'bg-bg-quaternary-10 text-text-tertiary border-border-tertiary-20'
|
||||
? 'bg-pink-500/10 text-pink-500 border-pink-500/20'
|
||||
: 'bg-zinc-500/10 text-zinc-500 border-zinc-500/20'
|
||||
]"
|
||||
>
|
||||
<component :is="stat.icon" :size="24" />
|
||||
@@ -28,7 +26,7 @@
|
||||
v-if="stat.trend"
|
||||
:class="[
|
||||
'flex items-center gap-1 text-[11px] font-bold',
|
||||
stat.trendDown ? 'text-error' : 'text-success'
|
||||
stat.trendDown ? 'text-red-400' : 'text-emerald-400'
|
||||
]"
|
||||
>
|
||||
<TrendingDown v-if="stat.trendDown" :size="12" />
|
||||
@@ -37,8 +35,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-text-tertiary text-sm font-medium">{{ stat.label }}</p>
|
||||
<h4 class="text-3xl font-bold tracking-tight text-text-primary">{{ stat.value }}</h4>
|
||||
<p class="text-zinc-500 text-sm font-medium">{{ stat.label }}</p>
|
||||
<h4 class="text-3xl font-bold tracking-tight text-zinc-100">{{ stat.value }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,19 +44,18 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<!-- 最近活动 -->
|
||||
<div
|
||||
class="lg:col-span-5 bg-bg-secondary-40 border border-border-secondary rounded-3xl overflow-hidden flex flex-col shadow-lg shadow-[0_10px_15px_var(--shadow-color)]"
|
||||
class="lg:col-span-5 bg-zinc-900/40 border border-zinc-800 rounded-3xl overflow-hidden flex flex-col shadow-lg shadow-black/20"
|
||||
>
|
||||
<div class="px-6 py-5 border-b border-border-secondary flex items-center justify-between">
|
||||
<div class="px-6 py-5 border-b border-zinc-800 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold flex items-center gap-2">
|
||||
<Activity :size="18" class="text-primary" /> {{ locale.recentActivities }}
|
||||
<Activity :size="18" class="text-blue-500" /> 最近活动
|
||||
</h3>
|
||||
<button
|
||||
class="p-2 text-text-tertiary hover:text-text-secondary transition-colors"
|
||||
:disabled="loadingActivities"
|
||||
class="p-2 text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
:class="{ 'animate-spin': loadingActivities }"
|
||||
@click="refreshActivities"
|
||||
>
|
||||
<RefreshCw v-if="!loadingActivities" :size="16" />
|
||||
<Loader2 v-else :size="16" class="animate-spin" />
|
||||
<RefreshCw :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -66,23 +63,23 @@
|
||||
>
|
||||
<div
|
||||
v-if="loadingActivities && recentActivities.length === 0"
|
||||
class="flex flex-col items-center justify-center h-full text-text-tertiary gap-3 py-20"
|
||||
class="flex flex-col items-center justify-center h-full text-zinc-500 gap-3 py-20"
|
||||
>
|
||||
<Loader2 :size="24" class="animate-spin" />
|
||||
<span class="text-sm">{{ locale.loading }}</span>
|
||||
<RefreshCw :size="24" class="animate-spin" />
|
||||
<span class="text-sm">加载中...</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="recentActivities.length === 0"
|
||||
class="flex flex-col items-center justify-center h-full text-text-tertiary gap-3 py-20"
|
||||
class="flex flex-col items-center justify-center h-full text-zinc-500 gap-3 py-20"
|
||||
>
|
||||
<Inbox :size="24" />
|
||||
<span class="text-sm">{{ locale.noActivities }}</span>
|
||||
<span class="text-sm">暂无活动记录</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(activity, idx) in recentActivities"
|
||||
:key="idx"
|
||||
class="flex items-start gap-4 p-4 rounded-2xl hover:bg-bg-tertiary-40 transition-all cursor-pointer group"
|
||||
class="flex items-start gap-4 p-4 rounded-2xl hover:bg-zinc-800/40 transition-all cursor-pointer group"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
@@ -94,14 +91,14 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h5
|
||||
class="font-bold text-sm text-text-primary group-hover:text-primary transition-colors"
|
||||
class="font-bold text-sm text-zinc-200 group-hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{{ activity.title }}
|
||||
</h5>
|
||||
<p class="text-xs text-text-tertiary truncate mt-1">{{ activity.description }}</p>
|
||||
<p class="text-xs text-zinc-500 truncate mt-1">{{ activity.description }}</p>
|
||||
<div class="flex items-center gap-1.5 mt-2">
|
||||
<Clock :size="10" class="text-text-disabled" />
|
||||
<span class="text-[10px] text-text-disabled font-medium uppercase tracking-wider">{{
|
||||
<Clock :size="10" class="text-zinc-600" />
|
||||
<span class="text-[10px] text-zinc-600 font-medium uppercase tracking-wider">{{
|
||||
formatTime(activity.createdAt)
|
||||
}}</span>
|
||||
</div>
|
||||
@@ -113,21 +110,21 @@
|
||||
|
||||
<!-- 系统状态 -->
|
||||
<div
|
||||
class="lg:col-span-4 bg-bg-secondary-40 border border-border-secondary rounded-xl overflow-hidden flex flex-col shadow-lg shadow-[0_10px_15px_var(--shadow-color)]"
|
||||
class="lg:col-span-4 bg-zinc-900/40 border border-zinc-800 rounded-xl overflow-hidden flex flex-col shadow-lg shadow-black/20"
|
||||
>
|
||||
<div class="px-6 py-5 border-b border-border-secondary flex items-center justify-between">
|
||||
<div class="px-6 py-5 border-b border-zinc-800 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold flex items-center gap-2">
|
||||
<ShieldCheck :size="18" class="text-success" /> {{ locale.systemStatus }}
|
||||
<ShieldCheck :size="18" class="text-emerald-500" /> 系统状态
|
||||
</h3>
|
||||
<span
|
||||
:class="[
|
||||
'px-3 py-1 text-[10px] font-black uppercase rounded-full border',
|
||||
systemStatus.online
|
||||
? 'bg-success-10 text-success border-success-20'
|
||||
: 'bg-error-10 text-error border-error-20'
|
||||
? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20'
|
||||
: 'bg-red-500/10 text-red-500 border-red-500/20'
|
||||
]"
|
||||
>
|
||||
{{ systemStatus.online ? locale.online : locale.offline }}
|
||||
{{ systemStatus.online ? '在线' : '离线' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
@@ -141,38 +138,24 @@
|
||||
:class="[
|
||||
'w-1.5 h-1.5 rounded-full',
|
||||
status.active
|
||||
? 'bg-success shadow-[0_0_10px_var(--success-70)]'
|
||||
: 'bg-bg-quaternary'
|
||||
? 'bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.7)]'
|
||||
: 'bg-zinc-600'
|
||||
]"
|
||||
/>
|
||||
<span class="text-xs font-semibold text-text-tertiary">{{ status.label }}</span>
|
||||
<span class="text-xs font-semibold text-zinc-400">{{ status.label }}</span>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-text-primary">{{ status.value }}</span>
|
||||
<span class="text-xs font-bold text-zinc-200">{{ status.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto border-t border-border-secondary px-6 py-4 flex flex-col items-center justify-center gap-1 text-center">
|
||||
<span class="text-[10px] font-medium uppercase tracking-[0.3em] text-text-disabled">
|
||||
{{ locale.instanceId }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="max-w-full text-xs text-text-tertiary hover:text-text-secondary transition-colors break-all leading-relaxed"
|
||||
:title="instanceId || locale.noInstanceId"
|
||||
:disabled="!instanceId"
|
||||
@click="copyInstanceId"
|
||||
>
|
||||
{{ instanceId || locale.noInstanceId }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div
|
||||
class="lg:col-span-3 bg-bg-secondary-40 border border-border-secondary rounded-3xl overflow-hidden flex flex-col shadow-lg shadow-[0_10px_15px_var(--shadow-color)]"
|
||||
class="lg:col-span-3 bg-zinc-900/40 border border-zinc-800 rounded-3xl overflow-hidden flex flex-col shadow-lg shadow-black/20"
|
||||
>
|
||||
<div class="px-6 py-5 border-b border-border-secondary">
|
||||
<div class="px-6 py-5 border-b border-zinc-800">
|
||||
<h3 class="text-lg font-bold flex items-center gap-2">
|
||||
<Zap :size="18" class="text-warning" /> {{ locale.quickActions }}
|
||||
<Zap :size="18" class="text-yellow-500" /> 快速操作
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6 space-y-3">
|
||||
@@ -182,8 +165,8 @@
|
||||
:class="[
|
||||
'w-full flex items-center gap-3 px-5 py-4 rounded-lg border font-bold text-sm transition-all text-left group',
|
||||
action.primary
|
||||
? 'bg-primary-hover border-primary text-text-primary shadow-xl shadow-[var(--primary-glow)] hover:bg-primary'
|
||||
: 'bg-bg-primary-40 border-border-secondary text-text-tertiary hover:border-border-tertiary hover:text-text-primary'
|
||||
? 'bg-blue-600 border-blue-500 text-white shadow-xl shadow-blue-900/20 hover:bg-blue-500'
|
||||
: 'bg-zinc-950/40 border-zinc-800 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200'
|
||||
]"
|
||||
@click="navigateTo(action.id)"
|
||||
>
|
||||
@@ -198,36 +181,31 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import {
|
||||
Activity,
|
||||
Ban,
|
||||
Bell,
|
||||
Calendar,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Heart,
|
||||
Inbox,
|
||||
Loader2,
|
||||
Music,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
Calendar,
|
||||
Heart,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap
|
||||
Activity,
|
||||
ShieldCheck,
|
||||
RefreshCw,
|
||||
Clock,
|
||||
Zap,
|
||||
Bell,
|
||||
Ban,
|
||||
ExternalLink,
|
||||
Inbox
|
||||
} from '@lucide/vue'
|
||||
import packageJson from '~~/package.json'
|
||||
import { useToast } from '~/composables/useToast'
|
||||
import { useLocale } from '~/utils/locale'
|
||||
|
||||
const emit = defineEmits(['navigate'])
|
||||
const { success: showSuccess, error: showError } = useToast()
|
||||
const { admin } = useLocale()
|
||||
const locale = computed(() => useSafeLocale(admin.value?.overview || {}))
|
||||
const { format: formatLocaleValue } = useLocaleText(locale)
|
||||
|
||||
const systemVersion = ref(packageJson.version)
|
||||
const { siteTitle } = useSiteConfig()
|
||||
const displayName = computed(() => siteTitle.value || '站点')
|
||||
const stats = ref({
|
||||
totalSongs: 0,
|
||||
totalUsers: 0,
|
||||
@@ -243,7 +221,6 @@ const stats = ref({
|
||||
|
||||
const recentActivities = ref([])
|
||||
const loadingActivities = ref(false)
|
||||
const instanceId = ref('')
|
||||
|
||||
const systemStatus = ref({
|
||||
online: true,
|
||||
@@ -254,7 +231,7 @@ const systemStatus = ref({
|
||||
// 统计卡片数据
|
||||
const statCards = computed(() => [
|
||||
{
|
||||
label: locale.value.statCards?.totalSongs || '总歌曲数',
|
||||
label: '总歌曲数',
|
||||
value: formatNumber(stats.value.totalSongs),
|
||||
icon: Music,
|
||||
color: 'blue',
|
||||
@@ -262,19 +239,19 @@ const statCards = computed(() => [
|
||||
trendDown: stats.value.songsChange < 0
|
||||
},
|
||||
{
|
||||
label: locale.value.statCards?.registeredUsers || '注册用户',
|
||||
label: '注册用户',
|
||||
value: formatNumber(stats.value.totalUsers),
|
||||
icon: Users,
|
||||
color: 'emerald'
|
||||
},
|
||||
{
|
||||
label: locale.value.statCards?.todaySchedules || '今日排班',
|
||||
label: '今日排期',
|
||||
value: formatNumber(stats.value.todaySchedules),
|
||||
icon: Calendar,
|
||||
color: 'warning'
|
||||
color: 'zinc'
|
||||
},
|
||||
{
|
||||
label: locale.value.statCards?.weeklyRequests || '本周点歌',
|
||||
label: '本周点歌',
|
||||
value: formatNumber(stats.value.weeklyRequests),
|
||||
icon: Heart,
|
||||
color: 'pink',
|
||||
@@ -286,49 +263,48 @@ const statCards = computed(() => [
|
||||
// 系统状态项
|
||||
const statusItems = computed(() => [
|
||||
{
|
||||
label: locale.value.statusItems?.database || '数据库',
|
||||
value: systemStatus.value.database ? locale.value.normal || '正常' : locale.value.abnormal || '异常',
|
||||
label: '数据库连接',
|
||||
value: systemStatus.value.database ? '正常' : '异常',
|
||||
active: systemStatus.value.database
|
||||
},
|
||||
{
|
||||
label: locale.value.statusItems?.api || 'API',
|
||||
value: systemStatus.value.api ? locale.value.normal || '正常' : locale.value.abnormal || '异常',
|
||||
label: 'API服务',
|
||||
value: systemStatus.value.api ? '正常' : '异常',
|
||||
active: systemStatus.value.api
|
||||
},
|
||||
{
|
||||
label: locale.value.statusItems?.semester || '当前学期',
|
||||
value: stats.value.currentSemester || locale.value.unset || '未设置',
|
||||
label: '当前学期',
|
||||
value: stats.value.currentSemester || '未设置',
|
||||
active: !!stats.value.currentSemester
|
||||
},
|
||||
{
|
||||
label: locale.value.statusItems?.blacklist || '黑名单',
|
||||
value: formatLocaleValue(locale.value.itemUnit, String(stats.value.blacklistCount), stats.value.blacklistCount),
|
||||
label: '黑名单项目',
|
||||
value: `${stats.value.blacklistCount} 项`,
|
||||
active: stats.value.blacklistCount >= 0
|
||||
},
|
||||
{ label: locale.value.statusItems?.version || '版本', value: `v${systemVersion.value}`, active: true }
|
||||
{ label: '站点版本', value: `${displayName.value} v${systemVersion.value}`, active: true }
|
||||
])
|
||||
|
||||
// 快速操作
|
||||
const quickActions = computed(() => [
|
||||
{ label: locale.value.actions?.schedule || '排班管理', icon: Calendar, id: 'schedule', primary: true },
|
||||
{ label: locale.value.actions?.users || '用户管理', icon: Users, id: 'users' },
|
||||
{ label: locale.value.actions?.notifications || '通知管理', icon: Bell, id: 'notifications' },
|
||||
{ label: locale.value.actions?.blacklist || '黑名单', icon: Ban, id: 'blacklist' }
|
||||
])
|
||||
const quickActions = [
|
||||
{ label: '管理排期', icon: Calendar, id: 'schedule', primary: true },
|
||||
{ label: '用户管理', icon: Users, id: 'users' },
|
||||
{ label: '发送通知', icon: Bell, id: 'notifications' },
|
||||
{ label: '黑名单管理', icon: Ban, id: 'blacklist' }
|
||||
]
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
|
||||
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const getActivityStyle = (type) => {
|
||||
const styles = {
|
||||
song: { icon: Music, bg: 'bg-info-10 text-info border-info-20' },
|
||||
user: { icon: Users, bg: 'bg-color-pink-alpha-10 text-color-pink border-color-pink-alpha-20' },
|
||||
schedule: { icon: Calendar, bg: 'bg-primary-10 text-primary border-primary-20' }
|
||||
song: { icon: Music, bg: 'bg-purple-500/10 text-purple-500 border-purple-500/20' },
|
||||
user: { icon: Users, bg: 'bg-pink-500/10 text-pink-500 border-pink-500/20' },
|
||||
schedule: { icon: Calendar, bg: 'bg-blue-500/10 text-blue-500 border-blue-500/20' }
|
||||
}
|
||||
|
||||
return styles[type] || { icon: Activity, bg: 'bg-bg-quaternary-10 text-text-tertiary border-border-tertiary-20' }
|
||||
return styles[type] || { icon: Activity, bg: 'bg-zinc-500/10 text-zinc-500 border-zinc-500/20' }
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
@@ -342,60 +318,6 @@ const loadStats = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadSystemStatus = async () => {
|
||||
try {
|
||||
const response = await $fetch('/api/system/status')
|
||||
systemStatus.value.online = response?.status === 'ok'
|
||||
systemStatus.value.database = !!response?.database?.connected
|
||||
systemStatus.value.api = response?.status === 'ok'
|
||||
instanceId.value = response?.instance?.instanceId || ''
|
||||
} catch (error) {
|
||||
console.error('加载系统状态失败:', error)
|
||||
systemStatus.value.online = false
|
||||
systemStatus.value.database = false
|
||||
systemStatus.value.api = false
|
||||
instanceId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', 'true')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
|
||||
const copied = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
return copied
|
||||
}
|
||||
|
||||
const copyInstanceId = async () => {
|
||||
if (!instanceId.value) {
|
||||
showError(locale.value.noInstanceId || '暂无实例 ID')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const copied = await copyToClipboard(instanceId.value)
|
||||
if (copied) {
|
||||
showSuccess(locale.value.copied || '已复制')
|
||||
} else {
|
||||
showError(locale.value.copyFailed || '复制失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('复制实例 ID 失败:', error)
|
||||
showError(locale.value.copyFailed || '复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadRecentActivities = async () => {
|
||||
loadingActivities.value = true
|
||||
try {
|
||||
@@ -417,20 +339,13 @@ const refreshActivities = () => {
|
||||
|
||||
const formatTime = (dateString) => {
|
||||
const date = new Date(dateString)
|
||||
const now = getSyncedDate()
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
if (diff < 60000) return locale.value?.justNow || '刚刚'
|
||||
if (diff < 3600000) {
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
return formatLocaleValue(locale.value?.minutesAgo, `${minutes}分钟前`, minutes)
|
||||
}
|
||||
if (diff < 86400000) {
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
return formatLocaleValue(locale.value?.hoursAgo, `${hours}小时前`, hours)
|
||||
}
|
||||
const days = Math.floor(diff / 86400000)
|
||||
return formatLocaleValue(locale.value?.daysAgo, `${days}天前`, days)
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
|
||||
return `${Math.floor(diff / 86400000)}天前`
|
||||
}
|
||||
|
||||
const navigateTo = (tab) => {
|
||||
@@ -439,7 +354,6 @@ const navigateTo = (tab) => {
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadSystemStatus()
|
||||
loadRecentActivities()
|
||||
})
|
||||
</script>
|
||||
@@ -452,10 +366,10 @@ onMounted(() => {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--panel-bg-alt);
|
||||
background: var(--bg-tertiary, #27272a);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--panel-bg-hover);
|
||||
background: #3f3f46;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user