feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+569
View File
@@ -0,0 +1,569 @@
From a82ff6747eeeea1afaac58f2c4c5e1c015c5b3ce Mon Sep 17 00:00:00 2001
From: HenryDu8133 <813367384@qq.com>
Date: Sat, 12 Sep 2026 11:56:30 +0800
Subject: [PATCH 47/58] =?UTF-8?q?feat(site):=20=E5=AF=B9=E9=BD=90=E5=8F=82?=
=?UTF-8?q?=E8=80=83=E7=AB=99=E7=82=B9UI=E4=B8=8E=E4=BA=A4=E4=BA=92?=
=?UTF-8?q?=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 页脚:响应耗时文字统一为「响应耗时 XXXms」,版本链接改为 GitHub 官方仓库,默认 ICP 备案号设置为粤ICP备2025450093号
- 顶部栏:恢复主题切换按钮与语言按钮(语言暂为占位提示),放置于用户区左侧
- 主题:修复首屏内联脚本默认主题白名单校验,避免 localStorage 中残留的 light/dark/System 等非法值生效,首屏默认改为 ClassicLight(仅允许 ClassicLight/ModernLight)
- 登录页:在密码输入框下方恢复「忘记密码?」入口,新增配套的找回密码引导页 /forgot-password
- 默认配置:system-settings 兜底记录与 SYSTEM_SETTINGS_DEFAULTS 新增 icpNumber 默认值
---
app/components/Auth/LoginForm.vue | 24 ++
app/components/SiteFooter.vue | 11 +-
app/pages/forgot-password.vue | 269 +++++++++++++++++++++++
app/pages/index.vue | 85 +++++++
nuxt.config.ts | 36 ++-
server/api/site-config.get.ts | 3 +-
server/utils/system-settings-defaults.ts | 2 +
7 files changed, 415 insertions(+), 15 deletions(-)
create mode 100644 app/pages/forgot-password.vue
diff --git a/app/components/Auth/LoginForm.vue b/app/components/Auth/LoginForm.vue
index 63472c8..4ec8d57 100644
--- a/app/components/Auth/LoginForm.vue
+++ b/app/components/Auth/LoginForm.vue
@@ -74,6 +74,11 @@
</svg>
</button>
</div>
+ <div class="forgot-password-row">
+ <NuxtLink to="/forgot-password" class="forgot-password-link">
+ 忘记密码?
+ </NuxtLink>
+ </div>
</div>
<div v-if="error" class="error-container">
@@ -556,6 +561,25 @@ const handleWebAuthnLogin = async () => {
height: 20px;
}
+.forgot-password-row {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 10px;
+}
+
+.forgot-password-link {
+ font-size: 13px;
+ color: var(--primary);
+ text-decoration: none;
+ font-weight: var(--font-medium);
+ transition: opacity 0.2s ease;
+}
+
+.forgot-password-link:hover {
+ opacity: 0.8;
+ text-decoration: underline;
+}
+
.form-footer {
margin-top: 24px;
text-align: center;
diff --git a/app/components/SiteFooter.vue b/app/components/SiteFooter.vue
index 0b3adc7..8036a69 100644
--- a/app/components/SiteFooter.vue
+++ b/app/components/SiteFooter.vue
@@ -23,11 +23,16 @@
</span>
<span v-if="siteTitle" class="footer-item">© {{ currentYear }} {{ siteTitle }}</span>
<span v-else class="footer-item">© {{ currentYear }} {{ copyrightOwner }}</span>
- <span class="footer-item">Worker in {{ responseTime }}ms</span>
+ <span class="footer-item">响应耗时 {{ responseTime }}ms</span>
<span class="footer-item">
- <NuxtLink class="voicehub-link" to="/changelog">
+ <a
+ class="voicehub-link"
+ href="https://github.com/laoshuikaixue/VoiceHub"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
{{ displayName }} v{{ systemVersion }}
- </NuxtLink>
+ </a>
</span>
<span v-if="isNetlify" class="footer-item">
<a
diff --git a/app/pages/forgot-password.vue b/app/pages/forgot-password.vue
new file mode 100644
index 0000000..62e6ee5
--- /dev/null
+++ b/app/pages/forgot-password.vue
@@ -0,0 +1,269 @@
+<template>
+ <div class="auth-layout">
+ <div class="auth-container">
+ <div class="form-section">
+ <div class="form-header">
+ <div class="logo-row">
+ <img
+ :src="brandLogoSrc"
+ alt="Brand Logo"
+ class="brand-logo-center"
+ width="96"
+ height="96"
+ decoding="async"
+ fetchpriority="high"
+ >
+ <div v-if="schoolLogoHomeUrl && schoolLogoHomeUrl.trim()" class="logo-divider" />
+ <img
+ v-if="schoolLogoHomeUrl && schoolLogoHomeUrl.trim()"
+ :src="schoolLogoHomeUrl"
+ alt="学校Logo"
+ class="school-logo"
+ width="160"
+ height="160"
+ decoding="async"
+ fetchpriority="high"
+ >
+ </div>
+ <h1 class="form-title">{{ siteTitle ? siteTitle + ' | VoiceHub' : 'VoiceHub' }}</h1>
+ <div class="header-divider" />
+ </div>
+ <div class="forgot-password-form">
+ <div class="page-intro">
+ <h2 class="page-heading">找回密码</h2>
+ <p class="page-subtitle">请联系管理员重置您的账号密码</p>
+ </div>
+
+ <div class="contact-card">
+ <div class="contact-card-icon">
+ <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
+ <path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
+ </svg>
+ </div>
+ <div class="contact-card-body">
+ <h3>联系管理员</h3>
+ <p>为保障账号安全,系统暂未开放自助密码找回。请通过以下方式联系管理员进行密码重置:</p>
+ <ul class="contact-tips">
+ <li>站内联系:登录有管理员权限的账号</li>
+ <li>线下联系:前往广播站办公室或指导老师</li>
+ <li>邮件联系:如有公开的管理员邮箱</li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="form-actions">
+ <NuxtLink to="/login" class="back-to-login-btn">
+ 返回登录
+ </NuxtLink>
+ </div>
+ </div>
+ </div>
+ </div>
+ <SiteFooter />
+ </div>
+</template>
+
+<script setup>
+import { onMounted, computed } from 'vue'
+import logo from '~~/public/images/logo.png'
+
+const { siteTitle, initSiteConfig, schoolLogoHomeUrl } = useSiteConfig()
+const brandLogoSrc = computed(() => {
+ return logo
+})
+
+onMounted(async () => {
+ await initSiteConfig()
+ if (typeof document !== 'undefined' && siteTitle.value) {
+ document.title = `找回密码 | ${siteTitle.value}`
+ }
+})
+</script>
+
+<style scoped>
+.auth-layout {
+ min-height: 100vh;
+ background: var(--bg-primary);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 20px;
+}
+
+.auth-container {
+ width: 100%;
+ max-width: 520px;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-2xl);
+ border: 1px solid var(--border-primary);
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+ margin: auto 0;
+ margin-bottom: 40px;
+}
+
+.form-section {
+ padding: 40px 32px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-secondary);
+}
+
+.form-header {
+ text-align: center;
+ margin-bottom: 24px;
+ width: 100%;
+}
+
+.logo-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 24px;
+ margin-bottom: 12px;
+}
+
+.logo-divider {
+ width: 1px;
+ height: 96px;
+ background: var(--border-secondary);
+}
+
+.brand-logo-center {
+ width: 96px;
+ height: 96px;
+ object-fit: contain;
+ filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15));
+}
+
+.school-logo {
+ width: 160px;
+ height: 160px;
+ object-fit: contain;
+ filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15));
+}
+
+.form-title {
+ font-size: 24px;
+ font-weight: var(--font-bold);
+ margin: 0;
+ color: var(--text-primary);
+}
+
+.header-divider {
+ height: 1px;
+ background: var(--border-secondary);
+ margin: 14px auto 0;
+ width: 100%;
+}
+
+.forgot-password-form {
+ width: 100%;
+ max-width: 400px;
+}
+
+.page-intro {
+ text-align: center;
+ margin-bottom: 28px;
+}
+
+.page-heading {
+ font-size: 28px;
+ font-weight: var(--font-bold);
+ color: var(--text-primary);
+ margin: 0 0 8px 0;
+}
+
+.page-subtitle {
+ font-size: 14px;
+ color: var(--text-tertiary);
+ margin: 0;
+}
+
+.contact-card {
+ display: flex;
+ gap: 16px;
+ padding: 24px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-secondary);
+ border-radius: var(--radius-lg);
+ margin-bottom: 28px;
+}
+
+.contact-card-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 12px;
+ background: rgba(59, 130, 246, 0.12);
+ color: var(--primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.contact-card-icon svg {
+ width: 20px;
+ height: 20px;
+}
+
+.contact-card-body h3 {
+ font-size: 16px;
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ margin: 0 0 8px 0;
+}
+
+.contact-card-body p {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin: 0 0 12px 0;
+ line-height: 1.6;
+}
+
+.contact-tips {
+ margin: 0;
+ padding-left: 18px;
+ color: var(--text-secondary);
+ font-size: 13px;
+ line-height: 1.9;
+}
+
+.form-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.back-to-login-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ padding: 14px;
+ background: var(--btn-primary-bg);
+ color: var(--btn-primary-text);
+ border: 1px solid var(--btn-primary-border);
+ border-radius: var(--radius-lg);
+ font-size: 16px;
+ font-weight: var(--font-semibold);
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+.back-to-login-btn:hover {
+ background: var(--btn-primary-hover);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+@media (max-width: 768px) {
+ .auth-layout { padding: 10px; }
+ .auth-container { border-radius: 16px; }
+ .form-section { padding: 30px 20px; }
+ .contact-card { padding: 18px; gap: 12px; }
+}
+</style>
diff --git a/app/pages/index.vue b/app/pages/index.vue
index df5d8a9..2a31f7e 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -33,6 +33,30 @@
<!-- 用户信息区域 -->
<div class="user-section">
<ClientOnly>
+ <!-- 主题切换按钮 -->
+ <button
+ class="utility-btn theme-toggle-btn"
+ type="button"
+ aria-label="主题"
+ title="切换主题"
+ @click="handleToggleTheme"
+ >
+ <Icon :name="isDark ? 'sun' : 'moon'" :size="18" />
+ <span class="utility-btn-label">主题</span>
+ </button>
+
+ <!-- 语言切换按钮(占位按钮,当前单语种) -->
+ <button
+ class="utility-btn lang-toggle-btn"
+ type="button"
+ aria-label="语言"
+ title="语言"
+ @click="handleLanguageClick"
+ >
+ <Icon name="globe" :size="18" />
+ <span class="utility-btn-label">语言</span>
+ </button>
+
<div v-if="isClientAuthenticated" class="user-info">
<div class="user-details-desktop">
<span class="user-name">{{ user?.name || '用户' }}</span>
@@ -674,12 +698,24 @@ import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
import { useNotifications } from '~/composables/useNotifications'
import { useSiteConfig } from '~/composables/useSiteConfig'
+import { useTheme } from '~/composables/useTheme'
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
// 获取运行时配置
const config = useRuntimeConfig()
const router = useRouter()
+// 主题
+const { isDark, toggleTheme } = useTheme()
+const handleToggleTheme = () => {
+ toggleTheme()
+}
+const handleLanguageClick = () => {
+ if (typeof window !== 'undefined' && window.$showNotification) {
+ window.$showNotification('当前版本暂不支持多语言切换', 'info')
+ }
+}
+
// 站点配置
const {
siteTitle,
@@ -1709,6 +1745,55 @@ if (
.user-section {
position: relative;
z-index: 100;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+/* 通用工具按钮(主题/语言) */
+.utility-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 12px;
+ background: var(--utility-btn-bg, rgba(255, 255, 255, 0.06));
+ color: var(--utility-btn-color, rgba(255, 255, 255, 0.78));
+ border: 1px solid var(--utility-btn-border, rgba(255, 255, 255, 0.12));
+ border-radius: 10px;
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 1;
+ cursor: pointer;
+ transition:
+ background 0.2s ease,
+ color 0.2s ease,
+ border-color 0.2s ease,
+ transform 0.15s ease;
+ backdrop-filter: blur(6px);
+}
+
+.utility-btn:hover {
+ background: var(--utility-btn-hover-bg, rgba(255, 255, 255, 0.12));
+ color: var(--utility-btn-hover-color, #ffffff);
+ border-color: var(--utility-btn-hover-border, rgba(255, 255, 255, 0.22));
+}
+
+.utility-btn:active {
+ transform: scale(0.97);
+}
+
+.utility-btn-label {
+ white-space: nowrap;
+}
+
+@media (max-width: 768px) {
+ .utility-btn-label {
+ display: none;
+ }
+ .utility-btn {
+ padding: 7px 8px;
+ border-radius: 9px;
+ }
}
.user-info {
diff --git a/nuxt.config.ts b/nuxt.config.ts
index d68f3fc..6964a0d 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -192,7 +192,11 @@ export default defineNuxtConfig({
{ name: 'theme-color', content: '#111111' },
{
name: 'x-default-theme',
- content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'System'
+ content: process.env.NUXT_PUBLIC_DEFAULT_THEME || 'ClassicLight'
+ },
+ {
+ name: 'x-enabled-themes',
+ content: process.env.NUXT_PUBLIC_ENABLED_THEMES || 'ClassicLight,ModernLight'
},
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
@@ -206,18 +210,28 @@ export default defineNuxtConfig({
tagPosition: 'head',
children: [
`(function(){try{`,
- ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null;`,
- ` try{s=localStorage.getItem(k)}catch(e){s=null}`,
- ` if(!s){`,
- ` try{`,
- ` var meta=document.querySelector('meta[name="x-default-theme"]');`,
- ` if(meta && meta.content){s=meta.content}`,
- ` }catch(e){s=null}`,
- ` if(!s){s='System'}`,
+ ` var k='voicehub:theme', d=document, r=d.documentElement, s=null, eff=null, def=null, list=[];`,
+ ` try{`,
+ ` var mt=d.querySelector('meta[name="x-enabled-themes"]');`,
+ ` if(mt && mt.content){list=String(mt.content).split(',').map(function(x){return x.replace(/^\\s+|\\s+$/g,'')}).filter(Boolean)}`,
+ ` }catch(e){list=[]}`,
+ ` try{`,
+ ` var md=d.querySelector('meta[name="x-default-theme"]');`,
+ ` if(md && md.content){def=md.content}`,
+ ` }catch(e){def=null}`,
+ ` if(!def){def='ClassicLight'}`,
+ ` if(list.length===0){list=['ClassicLight','ModernLight']}`,
+ ` function valid(t){`,
+ ` if(list.indexOf(t)!==-1){return t}`,
+ ` if(list.indexOf(def)!==-1){return def}`,
+ ` return list[0]`,
` }`,
- ` if(s==='System'||s==='system'){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
+ ` try{s=localStorage.getItem(k)}catch(e){s=null}`,
+ ` if(!s){s=def}`,
+ ` else{s=valid(s)}`,
+ ` if(typeof s==='string' && (s==='System'||s==='system')){eff=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}`,
` else if(typeof s==='string'){eff=s.endsWith('Dark')||s==='dark'?'dark':(s.endsWith('Light')||s==='light'?'light':s)}`,
- ` else{eff='dark'}`,
+ ` else{eff='light'}`,
` r.setAttribute('data-theme', (typeof s==='string'?s:eff));`,
` if(eff==='dark'){r.classList.add('dark'); r.classList.remove('light'); r.style.colorScheme='dark'}`,
` else if(eff==='light'){r.classList.add('light'); r.classList.remove('dark'); r.style.colorScheme='light'}`,
diff --git a/server/api/site-config.get.ts b/server/api/site-config.get.ts
index 0b427a6..0ad6af9 100644
--- a/server/api/site-config.get.ts
+++ b/server/api/site-config.get.ts
@@ -77,7 +77,8 @@ export default defineEventHandler(async (event) => {
siteDescription: '校园广播站点歌系统 - 让你的声音被听见',
siteVersion: process.env.NUXT_PUBLIC_SITE_VERSION || null,
submissionGuidelines: '请遵守校园规定,提交健康向上的歌曲。',
- icpNumber: null,
+ icpNumber: '粤ICP备2025450093号',
+ gonganNumber: null,
enableSubmissionLimit: false,
dailySubmissionLimit: null,
weeklySubmissionLimit: null,
diff --git a/server/utils/system-settings-defaults.ts b/server/utils/system-settings-defaults.ts
index 2627dca..73e2251 100644
--- a/server/utils/system-settings-defaults.ts
+++ b/server/utils/system-settings-defaults.ts
@@ -1,6 +1,8 @@
export const SYSTEM_SETTINGS_DEFAULTS = {
siteTitle: '校园广播站点歌系统',
siteVersion: '1.7.1.0',
+ icpNumber: '粤ICP备2025450093号',
+ gonganNumber: '',
enableSubmissionLimit: false,
showBlacklistKeywords: false,
hideStudentInfo: true,
--
2.45.1.windows.1