feat: WC2026 赛事 seed、生产上线初始化脚本与目录归档

重构 seed 为 WC2026 72 场小组赛与 48 强优胜盘;新增 production 模式仅保留 admin 与赛事示例;提供 prod-init-db 全量重置脚本;管理端 i18n 分包与赛事归档能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 18:17:00 +08:00
parent 8f14e85ebd
commit e7e938f261
94 changed files with 12332 additions and 976 deletions

View File

@@ -8,6 +8,7 @@ import { useAppLocale } from '../composables/useAppLocale';
import { useSmsCode } from '../composables/useSmsCode';
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
import GoldSpinner from '../components/GoldSpinner.vue';
import loginBg from '../assets/images/h5bg.webp';
const { t, locale } = useI18n();
@@ -104,7 +105,11 @@ const fieldError = () => {
<div class="login-lang">
<LocaleSwitcher compact />
</div>
<form @submit.prevent="submit" class="login-form ps-gold-frame">
<form @submit.prevent="submit" class="login-form ps-gold-frame" :class="{ 'is-busy': loading }">
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
<GoldSpinner :size="40" :active="true" />
<p class="form-busy-text">{{ t('auth.registering') }}</p>
</div>
<h2 class="form-title">{{ t('auth.register') }}</h2>
<div class="field">
@@ -116,6 +121,7 @@ const fieldError = () => {
autocomplete="username"
maxlength="32"
minlength="7"
:disabled="loading"
:placeholder="t('auth.username_register_placeholder')"
/>
</div>
@@ -130,6 +136,7 @@ const fieldError = () => {
type="tel"
required
autocomplete="tel-national"
:disabled="loading"
:placeholder="t('auth.phone_local_placeholder')"
/>
</div>
@@ -144,40 +151,55 @@ const fieldError = () => {
inputmode="numeric"
maxlength="6"
autocomplete="one-time-code"
:disabled="loading"
:placeholder="t('auth.sms_code_placeholder')"
/>
<button
type="button"
class="btn-secondary"
:disabled="sending || countdown > 0 || !phone.trim()"
:disabled="sending || countdown > 0 || !phone.trim() || loading"
@click="sendCode"
>
{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}
<span v-if="sending" class="btn-inline-loading">
<GoldSpinner :size="14" :active="true" />
{{ t('auth.sending_sms') }}
</span>
<span v-else>{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}</span>
</button>
</div>
</div>
<div class="field">
<label>{{ t('auth.password') }}</label>
<input v-model="password" class="field-input" type="password" required autocomplete="new-password" minlength="8" />
<input v-model="password" class="field-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
</div>
<div class="field">
<label>{{ t('auth.confirm_password') }}</label>
<input v-model="confirmPassword" class="field-input" type="password" required autocomplete="new-password" minlength="8" />
<input v-model="confirmPassword" class="field-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
</div>
<div class="field field-optional">
<label>{{ t('auth.invite_code') }} <span class="optional-tag">{{ t('auth.optional') }}</span></label>
<input v-model="inviteCode" class="field-input" autocomplete="off" />
<input v-model="inviteCode" class="field-input" autocomplete="off" :disabled="loading" />
</div>
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
<button type="submit" class="btn-login btn-gold-outline" :disabled="loading">
{{ t('auth.register_btn') }}
<button
type="submit"
class="btn-login btn-gold-outline"
:class="{ 'is-loading': loading }"
:disabled="loading"
:aria-busy="loading"
>
<span v-if="loading" class="btn-inline-loading">
<GoldSpinner :size="18" :active="true" />
{{ t('auth.registering') }}
</span>
<span v-else>{{ t('auth.register_btn') }}</span>
</button>
<button type="button" class="btn-skip" @click="goLogin">
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
{{ t('auth.have_account') }}
</button>
</form>
@@ -212,6 +234,7 @@ const fieldError = () => {
}
.login-form {
position: relative;
width: 100%;
max-width: 320px;
display: flex;
@@ -220,6 +243,40 @@ const fieldError = () => {
padding: 12px;
}
.login-form.is-busy {
pointer-events: none;
}
.form-busy-overlay {
position: absolute;
inset: 0;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: inherit;
background: rgba(8, 8, 8, 0.72);
backdrop-filter: blur(2px);
pointer-events: all;
}
.form-busy-text {
margin: 0;
font-size: 13px;
font-weight: 700;
color: rgba(240, 216, 117, 0.95);
letter-spacing: 0.04em;
}
.btn-inline-loading {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.form-title {
margin: 0 0 2px;
font-size: 16px;
@@ -315,6 +372,10 @@ label {
cursor: not-allowed;
}
.btn-login.is-loading:disabled {
opacity: 1;
}
.btn-skip {
margin-top: 2px;
padding: 8px 14px;