feat(client): align account and order foundations
This commit is contained in:
@@ -27,8 +27,8 @@ export interface ImpersonationExchangeResult {
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
export function loginMerchant(username: string, password: string) {
|
||||
return apiRequest<TokenPair>({ method: 'POST', url: '/auth/login', data: { username, password } })
|
||||
export function loginMerchant(loginId: string, password: string) {
|
||||
return apiRequest<TokenPair>({ method: 'POST', url: '/auth/login', data: { loginId, password } })
|
||||
}
|
||||
|
||||
export function registerMerchant(payload: RegisterMerchantRequest) {
|
||||
|
||||
@@ -149,8 +149,8 @@ export const useAuthStore = defineStore('client-auth', () => {
|
||||
sessionRestored.value = true
|
||||
}
|
||||
|
||||
async function login(username: string, password: string, remember = false) {
|
||||
const tokens = await loginMerchant(username, password)
|
||||
async function login(loginId: string, password: string, remember = false) {
|
||||
const tokens = await loginMerchant(loginId, password)
|
||||
const decoded = decodeMerchantToken(tokens.accessToken)
|
||||
persistTokenPair(tokens, remember)
|
||||
token.value = tokens.accessToken
|
||||
@@ -210,10 +210,9 @@ export const useAuthStore = defineStore('client-auth', () => {
|
||||
if (user.value) user.value = { ...user.value, name: next.realName }
|
||||
}
|
||||
|
||||
async function savePersonalProfile(next: Pick<ClientAccountProfile, 'realName' | 'phone' | 'email'>) {
|
||||
async function savePersonalProfile(next: Pick<ClientAccountProfile, 'realName' | 'email'>) {
|
||||
const updated = await updateMerchantPersonalProfile({
|
||||
displayName: next.realName.trim(),
|
||||
phone: next.phone.trim(),
|
||||
email: next.email.trim(),
|
||||
})
|
||||
updateProfile({
|
||||
@@ -237,5 +236,5 @@ export const useAuthStore = defineStore('client-auth', () => {
|
||||
if (event.key === 'cc-client-access-token' || event.key === 'cc-client-refresh-token') applyStoredSession()
|
||||
})
|
||||
|
||||
return { token, user, profile, permissionCodes, isAuthenticated, sessionRestored, hasPermission, login, logout, restoreSession, acceptImpersonationCode, updateProfile, savePersonalProfile }
|
||||
return { token, user, profile, permissionCodes, isAuthenticated, sessionRestored, hasPermission, login, logout, clearSession, restoreSession, acceptImpersonationCode, updateProfile, savePersonalProfile }
|
||||
})
|
||||
|
||||
@@ -30,9 +30,12 @@ test('groups the full registration contract without adding out-of-scope sign-in
|
||||
assert.doesNotMatch(loginSource + registerSource, /微信登录|企业微信登录|短信验证码|邮箱验证码|lang-switcher/)
|
||||
})
|
||||
|
||||
test('matches the backend username login contract', () => {
|
||||
assert.match(loginSource, /label="用户名"/)
|
||||
assert.match(loginSource, /\^\[A-Za-z0-9_\]/)
|
||||
test('matches the frozen username-or-mobile login contract', () => {
|
||||
assert.match(loginSource, /label="用户名或手机号"/)
|
||||
assert.match(loginSource, /const usernamePattern = \/\^\[A-Za-z\]/)
|
||||
assert.match(loginSource, /const mainlandMobilePattern = \/\^1\[3-9\]\\d\{9\}\$\//)
|
||||
assert.match(authApiSource, /data: \{ loginId, password \}/)
|
||||
assert.doesNotMatch(authApiSource, /data: \{ username, password \}/)
|
||||
assert.match(loginSource, /typeof route\.query\.account === 'string'/)
|
||||
assert.doesNotMatch(loginSource, /developmentHint|demo-tip|merchant_admin|联调账号|本地后端/)
|
||||
assert.match(loginSource, /form\.remember/)
|
||||
@@ -59,9 +62,9 @@ test('shows immediate login after registration and separates company review from
|
||||
assert.match(registerSource, /企业账号注册成功/)
|
||||
assert.match(registerSource, /无需等待注册审核/)
|
||||
assert.match(registerSource, /提交订单前还需完成企业审核/)
|
||||
assert.match(registerSource, /营业执照和法人身份证/)
|
||||
assert.match(registerSource, /货代资质证书、银行开户许可证可选填/)
|
||||
assert.match(registerSource, /其他资质可选填/)
|
||||
assert.match(registerSource, /首次登录需上传营业执照/)
|
||||
assert.match(registerSource, /法人身份证、货代资质证书和银行开户许可证可选填/)
|
||||
assert.match(registerSource, /法人身份证及其他资质可选填/)
|
||||
assert.match(registerSource, /立即登录/)
|
||||
})
|
||||
|
||||
@@ -113,6 +116,11 @@ test('shows the backend credential error on login', () => {
|
||||
assert.doesNotMatch(loginSource, /登录状态已失效,请重新登录/)
|
||||
})
|
||||
|
||||
test('shows a forced re-login notice after login identifiers change', () => {
|
||||
assert.match(loginSource, /route\.query\.reason === 'identifiers-updated'/)
|
||||
assert.match(loginSource, /所有会话均已退出,请重新登录/)
|
||||
})
|
||||
|
||||
test('keeps a valid local session during temporary context and refresh outages', () => {
|
||||
assert.match(authStoreSource, /function isExpiredSessionError/)
|
||||
assert.match(authStoreSource, /if \(isExpiredSessionError\(error\)\)/)
|
||||
|
||||
@@ -18,6 +18,7 @@ const auth = useAuthStore()
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const impersonationFailed = route.query.impersonation === 'failed'
|
||||
const loginIdentifiersUpdated = route.query.reason === 'identifiers-updated'
|
||||
const form = reactive<LoginForm>({
|
||||
account: typeof route.query.account === 'string'
|
||||
? route.query.account
|
||||
@@ -25,12 +26,23 @@ const form = reactive<LoginForm>({
|
||||
password: '',
|
||||
remember: false,
|
||||
})
|
||||
const accountPattern = /^[A-Za-z0-9_]{4,64}$/
|
||||
const usernamePattern = /^[A-Za-z][A-Za-z0-9_.@-]{3,63}$/
|
||||
const mainlandMobilePattern = /^1[3-9]\d{9}$/
|
||||
|
||||
function validLoginId(value: string) {
|
||||
return usernamePattern.test(value) || mainlandMobilePattern.test(value)
|
||||
}
|
||||
|
||||
const rules: FormRules<LoginForm> = {
|
||||
account: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ pattern: accountPattern, message: '用户名为 4–64 位字母、数字或下划线', trigger: 'blur' },
|
||||
{ required: true, message: '请输入用户名或手机号', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value || validLoginId(value.trim())) callback()
|
||||
else callback(new Error('请输入有效用户名或中国大陆手机号'))
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
@@ -76,7 +88,7 @@ function showResetHint() {
|
||||
<div class="auth-form-content auth-form-content--login">
|
||||
<div class="auth-form-heading">
|
||||
<h2 id="login-title">登录客户工作台</h2>
|
||||
<p>使用企业账号登录,继续管理您的跨境物流业务。</p>
|
||||
<p>使用用户名或中国大陆手机号登录,继续管理您的跨境物流业务。</p>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="impersonationFailed"
|
||||
@@ -85,10 +97,17 @@ function showResetHint() {
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-if="loginIdentifiersUpdated"
|
||||
title="用户名或手机号已更新,所有会话均已退出,请重新登录"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="auth-account-note">企业注册成功后即可登录;提交订单前需完成企业资料审核。</div>
|
||||
<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 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" />
|
||||
|
||||
@@ -29,7 +29,7 @@ const form = reactive<RegisterForm>({ username: '', companyName: '', displayName
|
||||
const rules: FormRules<RegisterForm> = {
|
||||
username: [
|
||||
{ required: true, message: '请输入登录用户名', trigger: 'blur' },
|
||||
{ pattern: /^[A-Za-z0-9_]{4,64}$/, message: '用户名为 4–64 位字母、数字或下划线', trigger: 'blur' },
|
||||
{ pattern: /^[A-Za-z][A-Za-z0-9_.@-]{3,63}$/, message: '用户名须以字母开头,并由 4–64 位字母、数字或 _.@- 组成', trigger: 'blur' },
|
||||
],
|
||||
companyName: [
|
||||
{ required: true, message: '请输入企业名称', trigger: 'blur' },
|
||||
@@ -48,7 +48,7 @@ const rules: FormRules<RegisterForm> = {
|
||||
],
|
||||
legalPhone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: '请输入有效的 11 位手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的中国大陆手机号', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请设置登录密码', trigger: 'blur' },
|
||||
@@ -113,7 +113,7 @@ async function submit() {
|
||||
<section class="register-section" aria-labelledby="account-section-title">
|
||||
<h2 id="account-section-title">账号信息</h2>
|
||||
<div class="register-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="username" required><el-input v-model="form.username" autocomplete="username" maxlength="64" placeholder="以字母开头,支持数字和 _.@-" /></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>
|
||||
</section>
|
||||
@@ -143,7 +143,7 @@ async function submit() {
|
||||
<span><RouterLink to="/legal/terms" target="_blank" rel="noopener">《用户服务协议》</RouterLink> 和 <RouterLink to="/legal/privacy" target="_blank" rel="noopener">《隐私政策》</RouterLink></span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<p class="register-security-note">注册成功后账号可立即登录;首次登录需上传营业执照和法人身份证,货代资质证书、银行开户许可证可选填。提交平台审核通过后才能提交订单。</p>
|
||||
<p class="register-security-note">注册成功后账号可立即登录;首次登录需上传营业执照,法人身份证、货代资质证书和银行开户许可证可选填。提交平台审核通过后才能提交订单。</p>
|
||||
<el-button native-type="submit" class="register-submit" type="success" :loading="submitting">注册企业账号</el-button>
|
||||
</el-form>
|
||||
</template>
|
||||
@@ -153,7 +153,7 @@ async function submit() {
|
||||
<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-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>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user