优化构架
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const loginSource = readFileSync(new URL('./LoginView.vue', import.meta.url), 'utf8')
|
||||
const registerSource = readFileSync(new URL('./RegisterView.vue', import.meta.url), 'utf8')
|
||||
const authStoreSource = readFileSync(new URL('../stores/auth.ts', import.meta.url), 'utf8')
|
||||
const httpSource = readFileSync(new URL('../api/http.ts', import.meta.url), 'utf8')
|
||||
const sessionSource = readFileSync(new URL('../api/session.ts', import.meta.url), 'utf8')
|
||||
const authApiSource = readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8')
|
||||
const mainSource = readFileSync(new URL('../main.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('matches the backend username login contract', () => {
|
||||
assert.match(loginSource, /label="用户名"/)
|
||||
assert.match(loginSource, /\^\[A-Za-z0-9_\]/)
|
||||
assert.match(loginSource, /typeof route\.query\.account === 'string'/)
|
||||
assert.doesNotMatch(loginSource, /developmentHint|demo-tip|merchant_admin|联调账号|本地后端/)
|
||||
assert.match(loginSource, /form\.remember/)
|
||||
assert.match(loginSource, /忘记密码/)
|
||||
})
|
||||
|
||||
test('persists both rotating tokens according to the remember choice', () => {
|
||||
assert.match(sessionSource, /ACCESS_TOKEN_KEY/)
|
||||
assert.match(sessionSource, /REFRESH_TOKEN_KEY/)
|
||||
assert.match(sessionSource, /remember \? localStorage : sessionStorage/)
|
||||
assert.match(sessionSource, /clearSessionTokens/)
|
||||
})
|
||||
|
||||
test('implements every backend registration field and real submission', () => {
|
||||
for (const field of ['username', 'companyName', 'displayName', 'legalPerson', 'legalPhone', 'contactEmail', 'countryCode', 'address', 'password', 'agreement']) {
|
||||
assert.match(registerSource, new RegExp(`prop="${field}"`))
|
||||
}
|
||||
assert.match(registerSource, /registerMerchant\(/)
|
||||
assert.match(registerSource, /4–64 位/)
|
||||
assert.match(registerSource, /8–72 位/)
|
||||
})
|
||||
|
||||
test('shows immediate login after registration and separates company review from account activation', () => {
|
||||
assert.match(registerSource, /企业账号注册成功/)
|
||||
assert.match(registerSource, /无需等待注册审核/)
|
||||
assert.match(registerSource, /提交订单前还需完成一步/)
|
||||
assert.match(registerSource, /立即登录/)
|
||||
})
|
||||
|
||||
test('uses real auth endpoints and single-flight refresh handling', () => {
|
||||
assert.match(authApiSource, /\/auth\/login/)
|
||||
assert.match(authApiSource, /\/auth\/register/)
|
||||
assert.match(authApiSource, /\/auth\/logout/)
|
||||
assert.match(httpSource, /refreshPromise/)
|
||||
assert.match(httpSource, /\/auth\/refresh/)
|
||||
assert.match(httpSource, /envelope\.code !== '200'/)
|
||||
assert.match(httpSource, /redirectToLoginAfterSessionExpiry/)
|
||||
assert.match(httpSource, /window\.location\.replace\(`\/login\?redirect=\$\{encodeURIComponent\(redirect\)\}`\)/)
|
||||
assert.match(authStoreSource, /decodeMerchantToken/)
|
||||
assert.doesNotMatch(authStoreSource, /mock-client:/)
|
||||
})
|
||||
|
||||
test('treats only body code 200 as success', () => {
|
||||
assert.match(httpSource, /if \(envelope\.code !== '200'\)/)
|
||||
assert.doesNotMatch(httpSource, /if \(envelope\.code !== '0'\)/)
|
||||
})
|
||||
|
||||
test('keeps a valid local session during temporary context and refresh outages', () => {
|
||||
assert.match(authStoreSource, /function isExpiredSessionError/)
|
||||
assert.match(authStoreSource, /if \(isExpiredSessionError\(error\)\)/)
|
||||
assert.match(authStoreSource, /sessionRestored\.value = true\s+return/)
|
||||
assert.match(httpSource, /if \(mappedError\.status === 401 \|\| mappedError\.code === 'A0401'\)/)
|
||||
assert.doesNotMatch(httpSource, /catch \(refreshError\) \{\s+clearSessionTokens\(\)/)
|
||||
})
|
||||
|
||||
test('loads styles for programmatic Element Plus messages and dialogs', () => {
|
||||
assert.match(mainSource, /element-plus\/es\/components\/message\/style\/css/)
|
||||
assert.match(mainSource, /element-plus\/es\/components\/message-box\/style\/css/)
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ArrowRight, LockKeyhole, UserRound } from '@lucide/vue'
|
||||
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
interface LoginForm {
|
||||
account: string
|
||||
password: string
|
||||
remember: boolean
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const form = reactive<LoginForm>({
|
||||
account: typeof route.query.account === 'string'
|
||||
? route.query.account
|
||||
: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
})
|
||||
const accountPattern = /^[A-Za-z0-9_]{4,64}$/
|
||||
|
||||
const rules: FormRules<LoginForm> = {
|
||||
account: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ pattern: accountPattern, message: '用户名为 4–64 位字母、数字或下划线', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 72, message: '密码长度应为 8–72 位', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!formRef.value || loading.value) return
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await auth.login(form.account.trim(), form.password, form.remember)
|
||||
ElMessage.success('登录成功')
|
||||
await router.replace(String(route.query.redirect || '/'))
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '登录失败,请检查账号后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showResetHint() {
|
||||
ElMessage.info('请联系平台客服重置密码')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<header class="auth-header">
|
||||
<RouterLink to="/login" class="auth-brand"><img class="auth-brand-logo" :src="baiyueLogoDark" alt="柏悦物流" /></RouterLink>
|
||||
<div>还没有企业账号? <RouterLink to="/register">申请入驻</RouterLink></div>
|
||||
</header>
|
||||
<main class="auth-main">
|
||||
<section class="auth-copy">
|
||||
<h1>跨境物流业务<br />一个工作台完成</h1>
|
||||
<p>从企业成员协作到头程订单跟进,统一管理业务与权限。</p>
|
||||
<div class="auth-facts"><span>企业级权限</span><span>订单全程跟进</span><span>多端业务协同</span></div>
|
||||
</section>
|
||||
<section class="auth-card" aria-labelledby="login-title">
|
||||
<h2 id="login-title">登录客户工作台</h2>
|
||||
<p>企业注册成功后即可登录;提交订单前需完成企业资料审核</p>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" size="large" @submit.prevent="submit">
|
||||
<el-form-item label="用户名" prop="account">
|
||||
<el-input v-model="form.account" autocomplete="username" maxlength="64" placeholder="请输入用户名" :prefix-icon="UserRound" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="form.password" autocomplete="current-password" maxlength="72" type="password" show-password placeholder="请输入 8–72 位密码" :prefix-icon="LockKeyhole" @keyup.enter="submit" />
|
||||
</el-form-item>
|
||||
<div class="auth-form-options">
|
||||
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||
<button type="button" @click="showResetHint">忘记密码?</button>
|
||||
</div>
|
||||
<el-button native-type="submit" class="auth-submit" type="primary" :loading="loading">登录 <ArrowRight :size="17" /></el-button>
|
||||
</el-form>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ArrowLeft, Building2, CheckCircle2, Mail, Phone } from '@lucide/vue'
|
||||
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
|
||||
import { registerMerchant } from '@/api/auth'
|
||||
|
||||
interface RegisterForm {
|
||||
username: string
|
||||
companyName: string
|
||||
displayName: string
|
||||
legalPerson: string
|
||||
legalPhone: string
|
||||
contactEmail: string
|
||||
countryCode: string
|
||||
address: string
|
||||
password: string
|
||||
agreement: boolean
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
const submittedAt = ref('')
|
||||
const form = reactive<RegisterForm>({ username: '', companyName: '', displayName: '', legalPerson: '', legalPhone: '', contactEmail: '', countryCode: 'CN', address: '', password: '', agreement: false })
|
||||
|
||||
const rules: FormRules<RegisterForm> = {
|
||||
username: [
|
||||
{ required: true, message: '请输入登录用户名', trigger: 'blur' },
|
||||
{ pattern: /^[A-Za-z0-9_]{4,64}$/, message: '用户名为 4–64 位字母、数字或下划线', trigger: 'blur' },
|
||||
],
|
||||
companyName: [
|
||||
{ required: true, message: '请输入企业名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '企业名称长度应为 2–100 个字符', trigger: 'blur' },
|
||||
],
|
||||
displayName: [
|
||||
{ required: true, message: '请输入联系人姓名', trigger: 'blur' },
|
||||
{ max: 64, message: '联系人姓名最多 64 个字符', trigger: 'blur' },
|
||||
],
|
||||
legalPerson: [
|
||||
{ required: true, message: '请输入法定代表人', trigger: 'blur' },
|
||||
{ max: 64, message: '法定代表人最多 64 个字符', trigger: 'blur' },
|
||||
],
|
||||
contactEmail: [
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
legalPhone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '请输入有效的 11 位手机号', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请设置登录密码', trigger: 'blur' },
|
||||
{ min: 8, max: 72, message: '密码长度应为 8–72 位', trigger: 'blur' },
|
||||
],
|
||||
agreement: [{ validator: (_rule, value, callback) => value ? callback() : callback(new Error('请先阅读并同意服务协议和隐私政策')), trigger: 'change' }],
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!formRef.value || submitting.value) return
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await registerMerchant({
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
displayName: form.displayName.trim(),
|
||||
companyName: form.companyName.trim(),
|
||||
legalPerson: form.legalPerson.trim(),
|
||||
legalPhone: form.legalPhone.trim(),
|
||||
contactEmail: form.contactEmail.trim() || undefined,
|
||||
countryCode: form.countryCode,
|
||||
address: form.address.trim() || undefined,
|
||||
})
|
||||
submittedAt.value = new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false,
|
||||
}).format(new Date())
|
||||
submitted.value = true
|
||||
ElMessage.success('企业账号注册成功')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '注册失败,请检查信息后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showDocument(name: string) {
|
||||
ElMessage.info(`${name}将在正式接入前由业务方确认内容`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="register-page">
|
||||
<header class="register-header">
|
||||
<RouterLink to="/login"><ArrowLeft :size="18" /> 返回登录</RouterLink>
|
||||
<div class="auth-brand"><img class="auth-brand-logo" :src="baiyueLogoDark" alt="柏悦物流" /></div>
|
||||
</header>
|
||||
<main class="register-main">
|
||||
<template v-if="!submitted">
|
||||
<div class="register-title"><div><Building2 :size="22" /></div><span><h1>企业注册</h1><p>注册成功后即可登录并使用客户工作台,提交订单前需完成企业资料审核。</p></span></div>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" class="register-form" label-position="top" size="large" @submit.prevent="submit">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="登录用户名" prop="username" required><el-input v-model="form.username" autocomplete="username" maxlength="64" placeholder="4–64 位字母、数字或下划线" /></el-form-item>
|
||||
<el-form-item label="企业名称" prop="companyName" required><el-input v-model="form.companyName" maxlength="100" placeholder="请输入企业营业执照上的名称" /></el-form-item>
|
||||
<el-form-item label="联系人" prop="displayName" required><el-input v-model="form.displayName" maxlength="64" placeholder="请输入联系人姓名" /></el-form-item>
|
||||
<el-form-item label="法定代表人" prop="legalPerson" required><el-input v-model="form.legalPerson" maxlength="64" placeholder="请输入营业执照法定代表人" /></el-form-item>
|
||||
<el-form-item label="法人手机号" prop="legalPhone" required><el-input v-model="form.legalPhone" type="tel" autocomplete="tel" inputmode="numeric" maxlength="11" placeholder="请输入手机号" :prefix-icon="Phone" /></el-form-item>
|
||||
<el-form-item label="联系邮箱" prop="contactEmail"><el-input v-model="form.contactEmail" type="email" autocomplete="email" maxlength="128" placeholder="选填,用于接收业务通知" :prefix-icon="Mail" /></el-form-item>
|
||||
<el-form-item label="国家/地区" prop="countryCode"><el-select v-model="form.countryCode"><el-option label="中国大陆(CN)" value="CN" /></el-select></el-form-item>
|
||||
<el-form-item label="企业地址" prop="address" class="full-row"><el-input v-model="form.address" maxlength="255" placeholder="选填,最多 255 个字符" /></el-form-item>
|
||||
<el-form-item label="登录密码" prop="password" required><el-input v-model="form.password" type="password" autocomplete="new-password" maxlength="72" show-password placeholder="请设置 8–72 位密码" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item prop="agreement" class="agreement-item">
|
||||
<div class="agreement-control">
|
||||
<el-checkbox v-model="form.agreement"><span class="agreement-required" aria-hidden="true">*</span>我已阅读并同意</el-checkbox>
|
||||
<span><button type="button" @click="showDocument('《用户服务协议》')">《用户服务协议》</button> 和 <button type="button" @click="showDocument('《隐私政策》')">《隐私政策》</button></span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<p class="register-security-note">注册成功后账号可立即登录;首次登录需上传营业执照并等待平台审核,审核通过后才能提交订单。</p>
|
||||
<div class="register-actions"><el-button @click="$router.push('/login')">取消</el-button><el-button native-type="submit" type="primary" :loading="submitting">注册企业账号</el-button></div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<section v-else class="registration-result" aria-live="polite">
|
||||
<CheckCircle2 :size="42" />
|
||||
<h1>企业账号注册成功</h1>
|
||||
<p><strong>{{ form.username }}</strong> 现在可以直接登录客户工作台,无需等待注册审核。</p>
|
||||
<dl><div><dt>企业名称</dt><dd>{{ form.companyName }}</dd></div><div><dt>登录账号</dt><dd>{{ form.username }}</dd></div><div><dt>注册时间</dt><dd>{{ submittedAt }}</dd></div><div><dt>账号状态</dt><dd><el-tag type="success" effect="light">可登录</el-tag></dd></div></dl>
|
||||
<div class="registration-next-step" role="note"><strong>提交订单前还需完成一步</strong><span>登录后请在“企业资料”补充公司和资质信息,等待平台人工审核通过。审核期间仍可维护资料、成员和订单草稿。</span></div>
|
||||
<div class="registration-result-actions"><el-button type="primary" @click="router.push({ path: '/login', query: { account: form.username } })">立即登录</el-button></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user