Initial Web IM deployment
This commit is contained in:
@@ -0,0 +1,677 @@
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<section class="login-panel">
|
||||
<div class="brand-lockup">
|
||||
<img class="brand-mark" :src="platformLogo" :alt="`${platformName} 图标`" @error="handleBrandLogoError">
|
||||
<div>
|
||||
<h1>{{ platformName }}</h1>
|
||||
<p>连接既有授权 IM 服务的新版 Web 客户端</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form v-if="view === 'login'" class="login-form" @submit.prevent="submit">
|
||||
<div class="login-tabs">
|
||||
<button type="button" :class="{ active: mode === 'code' }" @click="mode = 'code'">验证码登录</button>
|
||||
<button type="button" :class="{ active: mode === 'password' }" @click="mode = 'password'">密码登录</button>
|
||||
</div>
|
||||
<label>
|
||||
<span>手机号</span>
|
||||
<input v-model.trim="mobile" autocomplete="username" inputmode="tel" placeholder="请输入手机号">
|
||||
</label>
|
||||
<label v-if="mode === 'password'">
|
||||
<span>密码</span>
|
||||
<input v-model="password" autocomplete="current-password" type="password" placeholder="请输入密码">
|
||||
</label>
|
||||
<label v-else>
|
||||
<span>验证码</span>
|
||||
<div class="code-row">
|
||||
<input v-model.trim="code" autocomplete="one-time-code" inputmode="numeric" placeholder="本地测试请输入 66666">
|
||||
<button type="button" class="code-action" :disabled="sendingCode || mobile.length < 5" @click="requestCode">
|
||||
{{ sendingCode ? '发送中' : '发送验证码' }}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<button class="primary-action" :disabled="session.connecting || !canSubmit">
|
||||
{{ session.connecting ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
<p v-if="session.error" class="form-error">{{ session.error }}</p>
|
||||
<p v-if="info" class="form-info">{{ info }}</p>
|
||||
</form>
|
||||
|
||||
<form v-else-if="view === 'register'" class="login-form" @submit.prevent="submitRegister">
|
||||
<div class="login-tabs register-tabs" :style="{ gridTemplateColumns: `repeat(${visibleRegisterMethods.length || 1}, minmax(0, 1fr))` }">
|
||||
<button
|
||||
v-for="item in visibleRegisterMethods"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
:class="{ active: registerType === item.key }"
|
||||
@click="registerType = item.key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label v-if="registerType === 'phone'">
|
||||
<span class="label-row">
|
||||
<span>手机号</span>
|
||||
<span v-if="registerPhoneError" class="field-error-badge">{{ registerPhoneError }}</span>
|
||||
</span>
|
||||
<div class="phone-row">
|
||||
<div class="country-code-field">
|
||||
<span>+</span>
|
||||
<input
|
||||
v-model.trim="registerForm.phoneCountryCode"
|
||||
aria-label="手机号国码"
|
||||
autocomplete="tel-country-code"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
@input="normalizeRegisterCountryCode"
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
v-model.trim="registerForm.phone"
|
||||
:class="{ 'field-input-error': registerPhoneError }"
|
||||
autocomplete="tel"
|
||||
inputmode="tel"
|
||||
placeholder="请输入手机号"
|
||||
@input="clearRegisterPhoneError"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="registerType === 'phone'">
|
||||
<span>验证码</span>
|
||||
<div v-if="mobileRealVerify" class="code-row">
|
||||
<input v-model.trim="registerForm.code" autocomplete="one-time-code" inputmode="numeric" placeholder="请输入短信验证码">
|
||||
<button type="button" class="code-action" :disabled="sendingCode || registerForm.phone.length < 5" @click="requestRegisterCode">
|
||||
{{ sendingCode ? '发送中' : '发送验证码' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="captcha-row">
|
||||
<input
|
||||
v-model.trim="registerForm.code"
|
||||
:class="phoneCaptchaClass"
|
||||
autocomplete="off"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
placeholder="请输入验证码"
|
||||
@input="normalizePhoneCaptchaInput"
|
||||
>
|
||||
<button v-if="!phoneCaptchaImage" type="button" class="code-action" @click="showPhoneCaptcha">
|
||||
发送验证码
|
||||
</button>
|
||||
<button v-else type="button" class="captcha-button" title="点击更换验证码" @click="showPhoneCaptcha">
|
||||
<img :src="phoneCaptchaImage" alt="6位数字验证码">
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label v-if="emailRegisterEnabled && registerType === 'email'">
|
||||
<span>邮箱</span>
|
||||
<input v-model.trim="registerForm.email" autocomplete="email" inputmode="email" placeholder="请输入邮箱">
|
||||
</label>
|
||||
<label v-if="emailRegisterEnabled && registerType === 'email'">
|
||||
<span>邮箱验证码</span>
|
||||
<div class="code-row">
|
||||
<input v-model.trim="registerForm.emailCode" autocomplete="one-time-code" inputmode="numeric" placeholder="请输入验证码">
|
||||
<button type="button" class="code-action" disabled>发送验证码</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label v-if="registerType === 'username'">
|
||||
<span>用户名</span>
|
||||
<input v-model.trim="registerForm.username" autocomplete="username" placeholder="请输入用户名">
|
||||
</label>
|
||||
<label v-if="registerType === 'username'">
|
||||
<span>手机号</span>
|
||||
<div class="phone-row">
|
||||
<select v-model="registerForm.usernamePhoneCountryCode" aria-label="用户名注册手机号国码">
|
||||
<option v-for="item in countryCodeOptions" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</select>
|
||||
<input v-model.trim="registerForm.usernamePhone" autocomplete="tel" inputmode="tel" placeholder="请输入手机号">
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="registerType === 'username'">
|
||||
<span>验证码</span>
|
||||
<div class="captcha-row">
|
||||
<input
|
||||
v-model.trim="registerForm.usernameCode"
|
||||
:class="usernameCaptchaClass"
|
||||
autocomplete="off"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
placeholder="请输入验证码"
|
||||
@input="normalizeUsernameCaptchaInput"
|
||||
>
|
||||
<button v-if="!usernameCaptchaImage" type="button" class="code-action" @click="showUsernameCaptcha">
|
||||
显示验证码
|
||||
</button>
|
||||
<button v-else type="button" class="captcha-button" title="点击更换验证码" @click="showUsernameCaptcha">
|
||||
<img :src="usernameCaptchaImage" alt="6位数字验证码">
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label v-if="registerType === 'username'">
|
||||
<span>密码</span>
|
||||
<input v-model="registerForm.password" autocomplete="new-password" type="password" placeholder="请输入密码">
|
||||
</label>
|
||||
<label v-if="registerType === 'username'">
|
||||
<span>确认密码</span>
|
||||
<input v-model="registerForm.confirmPassword" autocomplete="new-password" type="password" placeholder="请再次输入密码">
|
||||
</label>
|
||||
|
||||
<button class="primary-action" :disabled="session.connecting || !canRegisterSubmit">
|
||||
{{ session.connecting ? '注册中...' : '注册' }}
|
||||
</button>
|
||||
<p v-if="session.error" class="form-error">{{ session.error }}</p>
|
||||
<p v-if="info" class="form-info">{{ info }}</p>
|
||||
</form>
|
||||
|
||||
<form v-else class="login-form" @submit.prevent="submitRegisterPassword">
|
||||
<label>
|
||||
<span>手机号</span>
|
||||
<input :value="pendingRegistration.mobile" autocomplete="username" readonly>
|
||||
</label>
|
||||
<label>
|
||||
<span>登录密码</span>
|
||||
<input v-model="registerPasswordForm.password" autocomplete="new-password" type="password" placeholder="请输入登录密码">
|
||||
</label>
|
||||
<label>
|
||||
<span>确认密码</span>
|
||||
<input v-model="registerPasswordForm.confirmPassword" autocomplete="new-password" type="password" placeholder="请再次输入密码">
|
||||
</label>
|
||||
<button class="primary-action" :disabled="session.connecting || settingRegisterPassword || !canSetRegisterPassword">
|
||||
{{ settingRegisterPassword || session.connecting ? '保存中...' : '保存并进入' }}
|
||||
</button>
|
||||
<p v-if="session.error" class="form-error">{{ session.error }}</p>
|
||||
<p v-if="info" class="form-info">{{ info }}</p>
|
||||
</form>
|
||||
|
||||
<button v-if="view === 'login'" class="ghost-action" type="button" @click="openRegister">
|
||||
新用户注册
|
||||
</button>
|
||||
<button v-else-if="view === 'register'" class="ghost-action" type="button" @click="backToLogin">
|
||||
返回登录
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import axios from 'axios';
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { appConfig } from '@/config';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
import { imClient } from '@/services/imClient';
|
||||
|
||||
const session = useSessionStore();
|
||||
const view = ref('login');
|
||||
const mode = ref('code');
|
||||
const mobile = ref('');
|
||||
const password = ref('');
|
||||
const code = ref('66666');
|
||||
const sendingCode = ref(false);
|
||||
const info = ref('');
|
||||
const pendingRegistrationStorageKey = 'qmt-web-im-pending-registration';
|
||||
const defaultPlatformName = appConfig.appName;
|
||||
const defaultPlatformLogo = '/img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico';
|
||||
const platformName = ref(defaultPlatformName);
|
||||
const platformLogo = ref(defaultPlatformLogo);
|
||||
const registerType = ref('phone');
|
||||
const emailRegisterEnabled = false;
|
||||
const registerMethods = ref({ phone: true, email: false, username: false });
|
||||
const mobileRealVerify = ref(false);
|
||||
const registerPhoneError = ref('');
|
||||
const phoneCaptcha = ref('');
|
||||
const phoneCaptchaImage = ref('');
|
||||
const usernameCaptcha = ref('');
|
||||
const usernameCaptchaImage = ref('');
|
||||
const settingRegisterPassword = ref(false);
|
||||
const pendingRegistration = reactive({
|
||||
mobile: '',
|
||||
result: null,
|
||||
resetCode: ''
|
||||
});
|
||||
const registerForm = reactive({
|
||||
phoneCountryCode: '86',
|
||||
phone: '',
|
||||
code: '',
|
||||
email: '',
|
||||
emailCode: '',
|
||||
username: '',
|
||||
usernamePhoneCountryCode: '+86',
|
||||
usernamePhone: '',
|
||||
usernameCode: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
const registerPasswordForm = reactive({
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
const countryCodeOptions = [
|
||||
{ value: '+86', label: '+86' },
|
||||
{ value: '+852', label: '+852' },
|
||||
{ value: '+853', label: '+853' },
|
||||
{ value: '+886', label: '+886' },
|
||||
{ value: '+1', label: '+1' },
|
||||
{ value: '+44', label: '+44' },
|
||||
{ value: '+60', label: '+60' },
|
||||
{ value: '+63', label: '+63' },
|
||||
{ value: '+65', label: '+65' },
|
||||
{ value: '+66', label: '+66' },
|
||||
{ value: '+81', label: '+81' },
|
||||
{ value: '+82', label: '+82' }
|
||||
];
|
||||
const registerMethodOptions = [
|
||||
{ key: 'phone', label: '手机号注册' },
|
||||
{ key: 'email', label: '邮箱注册' },
|
||||
{ key: 'username', label: '用户名注册' }
|
||||
];
|
||||
const canSubmit = computed(() => {
|
||||
if (mode.value === 'password') {
|
||||
return mobile.value.length >= 5 && password.value.length >= 1;
|
||||
}
|
||||
return mobile.value.length >= 5 && code.value.length >= 1;
|
||||
});
|
||||
const visibleRegisterMethods = computed(() => {
|
||||
const enabled = registerMethodOptions.filter(item => item.key !== 'email' && registerMethods.value[item.key]);
|
||||
return enabled.length ? enabled : [registerMethodOptions[0]];
|
||||
});
|
||||
const canRegisterSubmit = computed(() => {
|
||||
if (registerType.value === 'phone') {
|
||||
return registerForm.phone.length >= 5
|
||||
&& (mobileRealVerify.value ? registerForm.code.length >= 1 : phoneCaptchaMatched.value);
|
||||
}
|
||||
if (registerType.value === 'email') {
|
||||
return false;
|
||||
}
|
||||
return registerForm.username.length >= 3
|
||||
&& registerForm.usernamePhone.length >= 5
|
||||
&& usernameCaptchaMatched.value
|
||||
&& registerForm.password.length >= 1
|
||||
&& registerForm.password === registerForm.confirmPassword;
|
||||
});
|
||||
const canSetRegisterPassword = computed(() => {
|
||||
return pendingRegistration.mobile
|
||||
&& pendingRegistration.resetCode
|
||||
&& registerPasswordForm.password.length >= 1
|
||||
&& registerPasswordForm.password === registerPasswordForm.confirmPassword;
|
||||
});
|
||||
const phoneCaptchaStatus = computed(() => getCaptchaStatus(registerForm.code, phoneCaptcha.value));
|
||||
const phoneCaptchaMatched = computed(() => phoneCaptchaStatus.value === 'valid');
|
||||
const phoneCaptchaClass = computed(() => getCaptchaClass(phoneCaptchaStatus.value));
|
||||
const usernameCaptchaStatus = computed(() => {
|
||||
return getCaptchaStatus(registerForm.usernameCode, usernameCaptcha.value);
|
||||
});
|
||||
const usernameCaptchaMatched = computed(() => usernameCaptchaStatus.value === 'valid');
|
||||
const usernameCaptchaClass = computed(() => getCaptchaClass(usernameCaptchaStatus.value));
|
||||
|
||||
watch(visibleRegisterMethods, methods => {
|
||||
if (!methods.some(item => item.key === registerType.value)) {
|
||||
registerType.value = methods[0]?.key || 'phone';
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadPlatformConfig();
|
||||
loadRegisterConfig();
|
||||
restorePendingRegistration();
|
||||
});
|
||||
|
||||
async function requestCode() {
|
||||
info.value = '';
|
||||
sendingCode.value = true;
|
||||
try {
|
||||
await imClient.requestAuthCode(mobile.value);
|
||||
info.value = '验证码已发送;本地测试也可以直接使用 66666';
|
||||
} catch (error) {
|
||||
session.error = error.message || '验证码发送失败';
|
||||
} finally {
|
||||
sendingCode.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestRegisterCode() {
|
||||
info.value = '';
|
||||
sendingCode.value = true;
|
||||
try {
|
||||
await imClient.requestAuthCode(formatPhoneForApi(registerForm.phoneCountryCode, registerForm.phone));
|
||||
info.value = '验证码已发送;本地测试也可以直接使用 66666';
|
||||
} catch (error) {
|
||||
session.error = error.message || '验证码发送失败';
|
||||
} finally {
|
||||
sendingCode.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (mode.value === 'password') {
|
||||
session.loginWithPassword(mobile.value, password.value);
|
||||
return;
|
||||
}
|
||||
session.loginWithCode(mobile.value, code.value);
|
||||
}
|
||||
|
||||
function normalizeUsernameCaptchaInput() {
|
||||
registerForm.usernameCode = registerForm.usernameCode.replace(/\D/g, '').slice(0, 6);
|
||||
}
|
||||
|
||||
function normalizePhoneCaptchaInput() {
|
||||
registerForm.code = registerForm.code.replace(/\D/g, '').slice(0, 6);
|
||||
}
|
||||
|
||||
function normalizeRegisterCountryCode() {
|
||||
registerForm.phoneCountryCode = registerForm.phoneCountryCode.replace(/\D/g, '').slice(0, 6);
|
||||
}
|
||||
|
||||
function showPhoneCaptcha() {
|
||||
phoneCaptcha.value = randomDigits(6);
|
||||
phoneCaptchaImage.value = renderCaptchaImage(phoneCaptcha.value);
|
||||
registerForm.code = '';
|
||||
}
|
||||
|
||||
function showUsernameCaptcha() {
|
||||
usernameCaptcha.value = randomDigits(6);
|
||||
usernameCaptchaImage.value = renderCaptchaImage(usernameCaptcha.value);
|
||||
registerForm.usernameCode = '';
|
||||
}
|
||||
|
||||
function getCaptchaStatus(inputValue, captchaValue) {
|
||||
if (inputValue.length < 6) {
|
||||
return '';
|
||||
}
|
||||
return captchaValue && inputValue === captchaValue ? 'valid' : 'invalid';
|
||||
}
|
||||
|
||||
function getCaptchaClass(status) {
|
||||
return {
|
||||
'captcha-input-valid': status === 'valid',
|
||||
'captcha-input-invalid': status === 'invalid'
|
||||
};
|
||||
}
|
||||
|
||||
function randomDigits(length) {
|
||||
let value = '';
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
value += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderCaptchaImage(text) {
|
||||
const width = 112;
|
||||
const height = 42;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = '#f7fbfa';
|
||||
context.fillRect(0, 0, width, height);
|
||||
|
||||
for (let index = 0; index < 11; index += 1) {
|
||||
context.beginPath();
|
||||
context.moveTo(Math.random() * width, Math.random() * height);
|
||||
context.lineTo(Math.random() * width, Math.random() * height);
|
||||
context.strokeStyle = `rgba(${25 + Math.random() * 60}, ${55 + Math.random() * 70}, ${55 + Math.random() * 70}, 0.82)`;
|
||||
context.lineWidth = 1.4;
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
context.font = '700 23px Arial, sans-serif';
|
||||
context.textBaseline = 'middle';
|
||||
context.textAlign = 'center';
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
context.fillStyle = index % 2 === 0 ? '#153f3b' : '#1b6760';
|
||||
context.fillText(text[index], 14 + index * 17, height / 2 + 1);
|
||||
}
|
||||
|
||||
for (let index = 0; index < 70; index += 1) {
|
||||
context.fillStyle = `rgba(20, 70, 65, ${0.12 + Math.random() * 0.2})`;
|
||||
context.fillRect(Math.random() * width, Math.random() * height, 1.2, 1.2);
|
||||
}
|
||||
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
async function submitRegister() {
|
||||
session.error = '';
|
||||
info.value = '';
|
||||
if (registerType.value === 'phone') {
|
||||
clearRegisterPhoneError();
|
||||
if (!mobileRealVerify.value && !phoneCaptchaMatched.value) {
|
||||
session.error = '请输入正确的验证码';
|
||||
return;
|
||||
}
|
||||
const formattedPhone = formatPhoneForApi(registerForm.phoneCountryCode, registerForm.phone);
|
||||
const phoneExists = await isRegisterPhoneExists(formattedPhone);
|
||||
if (phoneExists === null) {
|
||||
return;
|
||||
}
|
||||
if (phoneExists) {
|
||||
registerPhoneError.value = '该手机号已经注册过';
|
||||
return;
|
||||
}
|
||||
const loginCode = mobileRealVerify.value ? registerForm.code : '66666';
|
||||
const result = await session.registerWithPhone(formattedPhone, loginCode);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
if (!result.resetCode) {
|
||||
session.error = '注册成功,但没有收到设置密码凭证';
|
||||
return;
|
||||
}
|
||||
setPendingRegistration(formattedPhone, result);
|
||||
registerPasswordForm.password = '';
|
||||
registerPasswordForm.confirmPassword = '';
|
||||
view.value = 'setPassword';
|
||||
return;
|
||||
}
|
||||
if (registerType.value === 'email') {
|
||||
session.error = '邮箱注册已暂时关闭,请使用手机号注册';
|
||||
return;
|
||||
}
|
||||
session.error = registerType.value === 'email'
|
||||
? '邮箱注册接口还未接入,请先使用手机号注册'
|
||||
: '用户名注册接口还未接入,请先使用手机号注册';
|
||||
}
|
||||
|
||||
async function submitRegisterPassword() {
|
||||
session.error = '';
|
||||
info.value = '';
|
||||
if (!pendingRegistration.mobile || !pendingRegistration.resetCode) {
|
||||
session.error = '注册状态已失效,请重新注册';
|
||||
return;
|
||||
}
|
||||
if (!registerPasswordForm.password || !registerPasswordForm.confirmPassword) {
|
||||
session.error = '请填写完整密码';
|
||||
return;
|
||||
}
|
||||
if (registerPasswordForm.password !== registerPasswordForm.confirmPassword) {
|
||||
session.error = '两次输入的密码不一致';
|
||||
return;
|
||||
}
|
||||
|
||||
settingRegisterPassword.value = true;
|
||||
try {
|
||||
await session.resetPasswordWithCode({
|
||||
mobile: pendingRegistration.mobile,
|
||||
resetCode: pendingRegistration.resetCode,
|
||||
newPassword: registerPasswordForm.password
|
||||
});
|
||||
await session.loginWithPassword(pendingRegistration.mobile, registerPasswordForm.password);
|
||||
if (!session.error) {
|
||||
clearPendingRegistration();
|
||||
}
|
||||
} catch (error) {
|
||||
session.error = error.message || '密码设置失败';
|
||||
} finally {
|
||||
settingRegisterPassword.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatPhoneForApi(countryCode, phone) {
|
||||
const digits = String(countryCode || '86').replace(/\D/g, '') || '86';
|
||||
const normalizedCode = `+${digits}`;
|
||||
const normalizedPhone = String(phone || '').replace(/\s/g, '');
|
||||
if (normalizedCode === '+86') {
|
||||
return normalizedPhone;
|
||||
}
|
||||
return `${normalizedCode}${normalizedPhone}`;
|
||||
}
|
||||
|
||||
async function loadRegisterConfig() {
|
||||
try {
|
||||
const response = await axios.get(appConfig.registerConfigApi, { withCredentials: false });
|
||||
const methods = response.data?.data?.methods;
|
||||
if (methods) {
|
||||
registerMethods.value = {
|
||||
phone: Boolean(methods.phone),
|
||||
email: emailRegisterEnabled && Boolean(methods.email),
|
||||
username: Boolean(methods.username)
|
||||
};
|
||||
}
|
||||
mobileRealVerify.value = Boolean(response.data?.data?.mobile_real_verify);
|
||||
} catch (error) {
|
||||
registerMethods.value = { phone: true, email: false, username: false };
|
||||
mobileRealVerify.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlatformConfig() {
|
||||
try {
|
||||
const response = await axios.get(appConfig.platformConfigApi, { withCredentials: false });
|
||||
if (response.data?.code !== 0) {
|
||||
throw new Error(response.data?.message || '平台配置加载失败');
|
||||
}
|
||||
const config = response.data?.result || {};
|
||||
const name = String(config.platformName || '').trim();
|
||||
const logo = String(config.logoUrl || config.logo || '').trim();
|
||||
const version = String(config.version || '').trim();
|
||||
platformName.value = name || defaultPlatformName;
|
||||
platformLogo.value = resolveBrandLogo(logo, version);
|
||||
} catch (error) {
|
||||
platformName.value = defaultPlatformName;
|
||||
platformLogo.value = defaultPlatformLogo;
|
||||
}
|
||||
applyDocumentBrand();
|
||||
}
|
||||
|
||||
function resolveBrandLogo(value, version = '') {
|
||||
const logo = String(value || '').trim();
|
||||
if (!logo) {
|
||||
return defaultPlatformLogo;
|
||||
}
|
||||
if (/^(https?:|data:|blob:)/i.test(logo)) {
|
||||
return appendBrandVersion(logo, version);
|
||||
}
|
||||
if (logo.startsWith('//')) {
|
||||
return appendBrandVersion(`${window.location.protocol}${logo}`, version);
|
||||
}
|
||||
const base = logo.startsWith('/') ? appConfig.adminServer : `${appConfig.adminServer}/`;
|
||||
return appendBrandVersion(new URL(logo, base).toString(), version);
|
||||
}
|
||||
|
||||
function appendBrandVersion(url, version) {
|
||||
if (!version || /^(data:|blob:)/i.test(url)) {
|
||||
return url;
|
||||
}
|
||||
const parsed = new URL(url, window.location.href);
|
||||
parsed.searchParams.set('v', version);
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function applyDocumentBrand() {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
document.title = platformName.value || defaultPlatformName;
|
||||
let icon = document.querySelector('link[rel="icon"]');
|
||||
if (!icon) {
|
||||
icon = document.createElement('link');
|
||||
icon.setAttribute('rel', 'icon');
|
||||
document.head.appendChild(icon);
|
||||
}
|
||||
icon.setAttribute('href', platformLogo.value || defaultPlatformLogo);
|
||||
}
|
||||
|
||||
function handleBrandLogoError() {
|
||||
if (platformLogo.value !== defaultPlatformLogo) {
|
||||
platformLogo.value = defaultPlatformLogo;
|
||||
applyDocumentBrand();
|
||||
}
|
||||
}
|
||||
|
||||
async function isRegisterPhoneExists(phone) {
|
||||
try {
|
||||
const response = await axios.post(appConfig.registerPhoneExistsApi, { mobile: phone }, { withCredentials: false });
|
||||
return Boolean(response.data?.data?.exists);
|
||||
} catch (error) {
|
||||
session.error = error.response?.data?.msg || error.message || '手机号检查失败,请稍后重试';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearRegisterPhoneError() {
|
||||
registerPhoneError.value = '';
|
||||
}
|
||||
|
||||
function clearPendingRegistration() {
|
||||
pendingRegistration.mobile = '';
|
||||
pendingRegistration.result = null;
|
||||
pendingRegistration.resetCode = '';
|
||||
registerPasswordForm.password = '';
|
||||
registerPasswordForm.confirmPassword = '';
|
||||
sessionStorage.removeItem(pendingRegistrationStorageKey);
|
||||
}
|
||||
|
||||
function setPendingRegistration(phone, result) {
|
||||
pendingRegistration.mobile = phone;
|
||||
pendingRegistration.result = result;
|
||||
pendingRegistration.resetCode = String(result.resetCode || '');
|
||||
sessionStorage.setItem(pendingRegistrationStorageKey, JSON.stringify({
|
||||
appServer: appConfig.appServer,
|
||||
mobile: pendingRegistration.mobile,
|
||||
resetCode: pendingRegistration.resetCode,
|
||||
result
|
||||
}));
|
||||
}
|
||||
|
||||
function restorePendingRegistration() {
|
||||
try {
|
||||
const pending = JSON.parse(sessionStorage.getItem(pendingRegistrationStorageKey) || 'null');
|
||||
if (!pending || pending.appServer !== appConfig.appServer || !pending.mobile || !pending.resetCode) {
|
||||
return;
|
||||
}
|
||||
pendingRegistration.mobile = pending.mobile;
|
||||
pendingRegistration.result = pending.result || null;
|
||||
pendingRegistration.resetCode = String(pending.resetCode || '');
|
||||
registerPasswordForm.password = '';
|
||||
registerPasswordForm.confirmPassword = '';
|
||||
view.value = 'setPassword';
|
||||
} catch (error) {
|
||||
sessionStorage.removeItem(pendingRegistrationStorageKey);
|
||||
}
|
||||
}
|
||||
|
||||
function openRegister() {
|
||||
session.error = '';
|
||||
info.value = '';
|
||||
clearRegisterPhoneError();
|
||||
clearPendingRegistration();
|
||||
registerForm.phone = registerForm.phone || mobile.value;
|
||||
registerForm.code = '';
|
||||
phoneCaptcha.value = '';
|
||||
phoneCaptchaImage.value = '';
|
||||
view.value = 'register';
|
||||
loadRegisterConfig();
|
||||
}
|
||||
|
||||
function backToLogin() {
|
||||
session.error = '';
|
||||
info.value = '';
|
||||
clearRegisterPhoneError();
|
||||
clearPendingRegistration();
|
||||
view.value = 'login';
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user