Files
Henry_Du cccaf36990 feat(brand): 站点品牌名统一为「宝一外校园点歌系统」
站长要求全站一致(小程序端已单独提交)。本次只改**用户可见文案与默认值**:

- 服务端默认值/兜底:system-settings-defaults、admin/system-settings(GET/POST)、
  site-config.get、utils/siteUtils —— 原来是 'VoiceHub' / '校园广播站点歌系统'
- 用户可见通知文案:MeoW 绑定验证码与标题、MeoW 测试通知、微信订阅测试通知
- 2FA 应用名(authenticator.keyuri)、WebAuthn rpName(出现在系统密钥弹窗里)
- 网页版界面:首页副标题、后台侧栏品牌名、登录/注册/找回/改密标题与文案、
  年度回顾(IntroSlide / ShareCard / 页面标题)、用户教程、通知发送预览、
  API 密钥与数据库导入说明、酷狗 sidecar 说明、打印页脚与歌单卡片页脚、Logo alt

**刻意不动**:localStorage/sessionStorage 的 `voicehub_*` 键、`voicehub:` 前缀的
STORAGE_KEY 与缓存/Redis 前缀、`window.voiceHubPlayer` 之类代码标识、MeoW 的
User-Agent、以及上游项目链接 `github.com/laoshuikaixue/VoiceHub`(那是对原项目的署名)。

注意:站点名**数据**(systemSettings.siteTitle)仍是你后台里设置的「宝一外校园点歌」,
代码只保证默认值与文案一致;要让全站显示完整名称,请在后台「站点配置 → 站点标题」
改成「宝一外校园点歌系统」。
2026-09-30 20:24:55 +08:00

374 lines
8.2 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="auth-layout">
<div class="auth-container">
<!-- 左侧信息区域 -->
<div class="info-section">
<div class="info-content">
<div class="logo-section">
<img
alt="宝一外校园点歌系统 Logo"
class="brand-logo"
:src="logo"
width="160"
height="160"
decoding="async"
fetchpriority="high"
>
<h1 v-if="siteTitle" class="brand-title">宝一外校园点歌系统</h1>
</div>
<div v-if="isFirstLogin" class="welcome-message">
<h2>欢迎使用宝一外校园点歌系统</h2>
<p>为了保障您的账号安全,请设置一个新的密码</p>
</div>
<div v-else class="security-message">
<h2>账号安全</h2>
<p>定期更新密码有助于保护您的账号安全</p>
</div>
<div class="security-tips">
<h3>密码安全建议</h3>
<div class="tip-list">
<div class="tip-item">
<svg
class="tip-icon"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<polyline points="20,6 9,17 4,12" />
</svg>
<span>至少8个字符</span>
</div>
<div class="tip-item">
<svg
class="tip-icon"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<polyline points="20,6 9,17 4,12" />
</svg>
<span>包含大小写字母</span>
</div>
<div class="tip-item">
<svg
class="tip-icon"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
>
<polyline points="20,6 9,17 4,12" />
</svg>
<span>包含数字和特殊字符</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧表单区域 -->
<div class="form-section">
<div class="form-container">
<div class="form-header">
<h2>{{ isFirstLogin ? '设置新密码' : '修改密码' }}</h2>
<p>{{ isFirstLogin ? '请设置一个安全的密码' : '更新您的登录密码' }}</p>
</div>
<ClientOnly>
<ChangePasswordForm :is-first-login="isFirstLogin" />
</ClientOnly>
<div class="form-footer">
<NuxtLink class="back-link" to="/">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<polyline points="15,18 9,12 15,6" />
</svg>
返回主页
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import ChangePasswordForm from '~/components/Auth/ChangePasswordForm.vue'
import { ref } from 'vue'
import logo from '~~/public/images/logo.png'
// 使用站点配置
const { siteTitle, initSiteConfig } = useSiteConfig()
const auth = useAuth()
const router = useRouter()
const isFirstLogin = ref(false)
// 未登录用户重定向到登录页
onMounted(async () => {
// 初始化站点配置
await initSiteConfig()
if (!auth.isAuthenticated.value && import.meta.client) {
router.push('/login')
return
}
// 检查是否需要修改密码(用于显示不同的UI提示)
if (import.meta.client) {
const userJson = localStorage.getItem('user')
if (userJson) {
const user = JSON.parse(userJson)
isFirstLogin.value = user.forcePasswordChange === true || !user.passwordChangedAt
}
}
})
</script>
<style scoped>
.auth-layout {
min-height: 100vh;
background: var(--bg-primary, #ffffff);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.auth-container {
width: 100%;
max-width: 1200px;
background: var(--surface-primary, var(--bg-primary, #ffffff));
border-radius: 24px;
border: 1px solid var(--border-primary, #e5e7eb);
overflow: hidden;
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 600px;
box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.1);
}
.info-section {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
padding: 60px 40px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.info-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
opacity: 0.3;
}
.info-content {
position: relative;
z-index: 1;
color: white;
text-align: center;
}
.logo-section {
margin-bottom: 40px;
}
.brand-logo {
width: 160px;
height: auto;
margin-bottom: 24px;
object-fit: contain;
}
.brand-title {
font-size: 36px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--card-bg, #ffffff) 0%, #f0f0f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.welcome-message,
.security-message {
margin-bottom: 40px;
}
.welcome-message h2,
.security-message h2 {
font-size: 24px;
font-weight: 600;
margin: 0 0 12px 0;
color: white;
}
.welcome-message p,
.security-message p {
font-size: 16px;
color: rgba(255, 255, 255, 0.9);
margin: 0;
line-height: 1.6;
}
.security-tips {
text-align: left;
}
.security-tips h3 {
font-size: 18px;
font-weight: 600;
color: white;
margin: 0 0 20px 0;
}
.tip-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.tip-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.tip-icon {
width: 16px;
height: 16px;
color: #10b981;
flex-shrink: 0;
}
.tip-item span {
font-size: 14px;
color: white;
}
.form-section {
padding: 60px 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-primary, var(--bg-primary, #ffffff));
}
.form-container {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
}
.form-header {
text-align: center;
margin-bottom: 32px;
}
.form-header h2 {
font-size: 28px;
font-weight: 700;
color: var(--text-primary, #1a2332);
margin: 0 0 8px 0;
}
.form-header p {
font-size: 16px;
color: var(--text-secondary, #4a5568);
margin: 0;
}
.form-footer {
margin-top: 32px;
text-align: center;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: var(--surface-secondary, var(--bg-secondary, #f7fafc));
border: 1px solid var(--border-primary, #e5e7eb);
border-radius: 8px;
color: var(--text-secondary, #4a5568);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.back-link:hover {
background: var(--surface-tertiary, var(--bg-tertiary, #edf2f7));
color: var(--text-primary, #1a2332);
border-color: var(--border-secondary, #cbd5e1);
}
.back-link svg {
width: 16px;
height: 16px;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.auth-container {
grid-template-columns: 1fr;
max-width: 500px;
}
.info-section {
padding: 40px 30px;
}
.form-section {
padding: 40px 30px;
}
.brand-title {
font-size: 28px;
}
}
@media (max-width: 768px) {
.auth-layout {
padding: 10px;
}
.auth-container {
border-radius: 16px;
min-height: auto;
}
.info-section,
.form-section {
padding: 30px 20px;
}
.tip-list {
gap: 8px;
}
.tip-item {
padding: 8px 12px;
}
}
</style>