feat: 迁移自定义功能至 1.5.9.9 版本

包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
2026-09-12 19:26:42 +08:00
parent 34f0679117
commit 1106aeea6c
161 changed files with 18963 additions and 27900 deletions
+143 -108
View File
@@ -4,12 +4,12 @@
<div v-if="!isFirstLogin" class="space-y-2">
<label
for="current-password"
class="text-xs font-black text-text-tertiary uppercase tracking-widest ml-1"
>{{ locale.currentPassword }}</label
class="text-xs font-black text-zinc-500 uppercase tracking-widest ml-1"
>当前密码</label
>
<div class="relative group">
<div
class="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary group-focus-within:text-primary transition-colors"
class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-blue-500 transition-colors"
>
<Lock :size="18" />
</div>
@@ -19,21 +19,17 @@
:class="[
inputClass,
error
? 'border-error shadow-[0_0_15px_var(--auth-error-input-shadow)]'
: 'border-border-secondary focus:border-primary-30'
? 'border-rose-500 shadow-[0_0_15px_rgba(244,63,94,0.1)]'
: 'border-zinc-800 focus:border-blue-500/30'
]"
:type="showCurrentPassword ? 'text' : 'password'"
autocomplete="current-password"
:placeholder="locale.currentPasswordPlaceholder"
placeholder="请输入当前密码"
required
@input="error = ''"
/>
>
<button
class="absolute right-4 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary transition-colors"
class="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
type="button"
:aria-label="
showCurrentPassword ? locale.hideCurrentPassword : locale.showCurrentPassword
"
@click="showCurrentPassword = !showCurrentPassword"
>
<Eye v-if="!showCurrentPassword" :size="18" />
@@ -45,12 +41,12 @@
<div class="space-y-2">
<label
for="new-password"
class="text-xs font-black text-text-tertiary uppercase tracking-widest ml-1"
>{{ locale.newPassword }}</label
class="text-xs font-black text-zinc-500 uppercase tracking-widest ml-1"
>新密码</label
>
<div class="relative group">
<div
class="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary group-focus-within:text-primary transition-colors"
class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-blue-500 transition-colors"
>
<KeyRound :size="18" />
</div>
@@ -60,19 +56,20 @@
:class="[
inputClass,
error
? 'border-error shadow-[0_0_15px_var(--auth-error-input-shadow)]'
: 'border-border-secondary focus:border-primary-30'
? 'border-rose-500 shadow-[0_0_15px_rgba(244,63,94,0.1)]'
: 'border-zinc-800 focus:border-blue-500/30'
]"
:type="showNewPassword ? 'text' : 'password'"
autocomplete="new-password"
:placeholder="locale.newPasswordPlaceholder"
placeholder="请输入新密码"
required
@input="handleNewPasswordInput"
/>
@input="
error = '';
validatePassword()
"
>
<button
class="absolute right-4 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary transition-colors"
class="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
type="button"
:aria-label="showNewPassword ? locale.hideNewPassword : locale.showNewPassword"
@click="showNewPassword = !showNewPassword"
>
<Eye v-if="!showNewPassword" :size="18" />
@@ -82,7 +79,7 @@
<!-- 密码强度指示器 -->
<div v-if="newPassword" class="px-1 pt-1 space-y-2">
<div class="h-1 w-full bg-bg-tertiary rounded-full overflow-hidden">
<div class="h-1 w-full bg-zinc-800 rounded-full overflow-hidden">
<div
class="h-full transition-all duration-500"
:class="passwordStrength.colorClass"
@@ -90,8 +87,8 @@
/>
</div>
<div class="flex justify-between items-center">
<span class="text-[10px] font-black uppercase tracking-widest text-text-tertiary"
>{{ locale.passwordStrength }}</span
<span class="text-[10px] font-black uppercase tracking-widest text-zinc-500"
>密码强度</span
>
<span
class="text-[10px] font-black uppercase tracking-widest"
@@ -106,12 +103,12 @@
<div class="space-y-2">
<label
for="confirm-password"
class="text-xs font-black text-text-tertiary uppercase tracking-widest ml-1"
>{{ locale.confirmPassword }}</label
class="text-xs font-black text-zinc-500 uppercase tracking-widest ml-1"
>确认新密码</label
>
<div class="relative group">
<div
class="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary group-focus-within:text-primary transition-colors"
class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500 group-focus-within:text-blue-500 transition-colors"
>
<CheckCircle2 :size="18" />
</div>
@@ -121,21 +118,17 @@
:class="[
inputClass,
error || (confirmPassword && newPassword !== confirmPassword)
? 'border-error shadow-[0_0_15px_var(--auth-error-input-shadow)]'
: 'border-border-secondary focus:border-primary-30'
? 'border-rose-500 shadow-[0_0_15px_rgba(244,63,94,0.1)]'
: 'border-zinc-800 focus:border-blue-500/30'
]"
:type="showConfirmPassword ? 'text' : 'password'"
autocomplete="new-password"
:placeholder="locale.confirmPasswordPlaceholder"
placeholder="请再次输入新密码"
required
@input="error = ''"
/>
>
<button
class="absolute right-4 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-secondary transition-colors"
class="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
type="button"
:aria-label="
showConfirmPassword ? locale.hideConfirmPassword : locale.showConfirmPassword
"
@click="showConfirmPassword = !showConfirmPassword"
>
<Eye v-if="!showConfirmPassword" :size="18" />
@@ -147,14 +140,14 @@
<div v-if="confirmPassword" class="px-1">
<div
v-if="newPassword !== confirmPassword"
class="flex items-center gap-1.5 text-error"
class="flex items-center gap-1.5 text-rose-500"
>
<XCircle :size="12" />
<span class="text-[10px] font-bold">{{ locale.mismatch }}</span>
<span class="text-[10px] font-bold">密码不匹配</span>
</div>
<div v-else class="flex items-center gap-1.5 text-success">
<div v-else class="flex items-center gap-1.5 text-emerald-500">
<CheckCircle2 :size="12" />
<span class="text-[10px] font-bold">{{ locale.matched }}</span>
<span class="text-[10px] font-bold">密码匹配</span>
</div>
</div>
</div>
@@ -162,36 +155,34 @@
<!-- 状态消息 -->
<div
v-if="error"
aria-live="polite"
class="p-3 bg-error-10 border border-error-20 rounded-xl flex items-center gap-3"
class="p-3 bg-rose-500/10 border border-rose-500/20 rounded-xl flex items-center gap-3"
>
<AlertCircle :size="16" class="text-error shrink-0" />
<span class="text-xs text-error font-medium">{{ error }}</span>
<AlertCircle :size="16" class="text-rose-500 shrink-0" />
<span class="text-xs text-rose-500 font-medium">{{ error }}</span>
</div>
<div
v-if="success"
aria-live="polite"
class="p-3 bg-success-10 border border-success-20 rounded-xl flex items-center gap-3"
class="p-3 bg-emerald-500/10 border border-emerald-500/20 rounded-xl flex items-center gap-3"
>
<CheckCircle2 :size="16" class="text-success shrink-0" />
<span class="text-xs text-success font-medium">{{ success }}</span>
<CheckCircle2 :size="16" class="text-emerald-500 shrink-0" />
<span class="text-xs text-emerald-500 font-medium">{{ success }}</span>
</div>
<button
:disabled="loading || !isFormValid"
class="w-full flex items-center justify-center gap-2 py-3 bg-primary-hover hover:bg-primary text-text-primary text-sm font-black rounded-xl shadow-lg shadow-[var(--primary-glow)] transition-all active:scale-[0.98] disabled:opacity-50"
class="w-full flex items-center justify-center gap-2 py-3 bg-blue-600 hover:bg-blue-500 text-white text-sm font-black rounded-xl shadow-lg shadow-blue-900/20 transition-all active:scale-[0.98] disabled:opacity-50"
type="submit"
>
<Loader2 v-if="loading" :size="18" class="animate-spin" />
<span>{{ loading ? locale.processing : isFirstLogin ? locale.setInitial : locale.submitChange }}</span>
<span>{{ loading ? '处理中...' : isFirstLogin ? '设置初始密码' : '确认修改密码' }}</span>
</button>
</form>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, ref } from 'vue'
import { computed, ref } from 'vue'
import {
Lock,
KeyRound,
@@ -202,9 +193,6 @@ import {
AlertCircle,
Loader2
} from '@lucide/vue'
import { useLocale } from '~/utils/locale'
import { usePasswordStrength } from '~/composables/usePasswordStrength'
import { validateInitialPasswordPolicy, validatePasswordPolicy } from '~/utils/password-policy'
// 组件属性
const props = defineProps({
@@ -216,9 +204,6 @@ const props = defineProps({
const auth = useAuth()
const router = useRouter()
const { auth: authLocale } = useLocale()
const locale = computed(() => authLocale.value?.changePasswordForm || {})
const { localize: localizeServerError } = useServerErrors()
const currentPassword = ref('')
const newPassword = ref('')
@@ -226,20 +211,10 @@ const confirmPassword = ref('')
const error = ref('')
const success = ref('')
const loading = ref(false)
let redirectTimer = null
const scheduleRedirect = (callback) => {
if (redirectTimer) clearTimeout(redirectTimer)
redirectTimer = setTimeout(callback, 2000)
}
onBeforeUnmount(() => {
if (redirectTimer) clearTimeout(redirectTimer)
})
// 样式类
const inputClass =
'w-full bg-bg-primary border rounded-xl pl-11 pr-11 py-3 text-sm text-text-primary placeholder:text-text-disabled focus:outline-none transition-all'
'w-full bg-zinc-950 border rounded-xl pl-11 pr-11 py-3 text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none transition-all'
// 密码显示状态
const showCurrentPassword = ref(false)
@@ -247,43 +222,84 @@ const showNewPassword = ref(false)
const showConfirmPassword = ref(false)
// 密码强度计算
const passwordStrength = usePasswordStrength(newPassword)
const passwordStrength = computed(() => {
const password = newPassword.value
if (!password) return { width: '0%', colorClass: '', textColorClass: '', text: '' }
const passwordPolicyError = computed(() =>
props.isFirstLogin
? validateInitialPasswordPolicy(newPassword.value)
: validatePasswordPolicy(newPassword.value)
)
let score = 0
if (password.length >= 8) score += 25
if (/[A-Z]/.test(password)) score += 25
if (/[a-z]/.test(password)) score += 25
if (/[0-9]/.test(password) && /[^A-Za-z0-9]/.test(password)) score += 25
if (score < 50) {
return {
width: `${score || 10}%`,
colorClass: 'bg-rose-500',
textColorClass: 'text-rose-500',
text: '弱'
}
} else if (score < 75) {
return {
width: `${score}%`,
colorClass: 'bg-amber-500',
textColorClass: 'text-amber-500',
text: '中等'
}
} else if (score < 100) {
return {
width: `${score}%`,
colorClass: 'bg-blue-500',
textColorClass: 'text-blue-500',
text: '强'
}
} else {
return {
width: '100%',
colorClass: 'bg-emerald-500',
textColorClass: 'text-emerald-500',
text: '极强'
}
}
})
// 表单验证
const isFormValid = computed(() => {
return Boolean(
(props.isFirstLogin || currentPassword.value) &&
newPassword.value &&
confirmPassword.value &&
newPassword.value === confirmPassword.value &&
!passwordPolicyError.value
)
if (props.isFirstLogin) {
return (
newPassword.value &&
confirmPassword.value &&
newPassword.value === confirmPassword.value &&
newPassword.value.length >= 8
)
} else {
return (
currentPassword.value &&
newPassword.value &&
confirmPassword.value &&
newPassword.value === confirmPassword.value &&
newPassword.value.length >= 8
)
}
})
const handleNewPasswordInput = () => {
error.value = newPassword.value ? passwordPolicyError.value || '' : ''
}
const resetForm = () => {
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
const validatePassword = () => {
if (newPassword.value && newPassword.value.length < 8) {
error.value = '密码长度至少为8位'
} else {
error.value = ''
}
}
const handleChangePassword = async () => {
if (newPassword.value !== confirmPassword.value) {
error.value = locale.value.newPasswordMismatch
error.value = '新密码和确认密码不匹配'
return
}
if (passwordPolicyError.value) {
error.value = passwordPolicyError.value
if (newPassword.value.length < 8) {
error.value = '新密码长度至少为8位'
return
}
@@ -294,33 +310,52 @@ const handleChangePassword = async () => {
try {
if (props.isFirstLogin) {
await auth.setInitialPassword(newPassword.value)
success.value = locale.value.initialSuccess
success.value = '密码设置成功!正在跳转...'
resetForm()
// 清空表单
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
// 密码设置完成后跳转
scheduleRedirect(async () => {
setTimeout(async () => {
// 更新用户状态
await auth.refreshUser()
if (auth.isAdmin.value) {
router.replace('/dashboard')
router.push('/dashboard')
} else {
router.replace('/')
router.push('/')
}
})
}, 2000)
} else {
await auth.changePassword(currentPassword.value, newPassword.value)
success.value = locale.value.changeSuccess
success.value = '密码修改成功!请重新登录'
resetForm()
// 清空表单
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
// 密码修改后登出
scheduleRedirect(async () => {
await auth.logout(false)
await router.replace('/login')
})
setTimeout(() => {
auth.logout()
router.push('/login')
}, 2000)
}
} catch (err) {
// 统一按错误码本地化服务端错误,未命中再回退到默认文案
error.value = localizeServerError(err, locale.value.failed)
// 提取错误信息,支持多种错误格式
if (err.data && err.data.statusMessage) {
error.value = err.data.statusMessage
} else if (err.data && err.data.message) {
error.value = err.data.message
} else if (err.statusMessage) {
error.value = err.statusMessage
} else if (err.message) {
error.value = err.message
} else {
error.value = '操作失败,请重试'
}
} finally {
loading.value = false
}