diff --git a/src/api/wechat/index.ts b/src/api/wechat/index.ts index 6507f03..0d1f75c 100644 --- a/src/api/wechat/index.ts +++ b/src/api/wechat/index.ts @@ -69,6 +69,12 @@ export async function wxLogin(loginUuid: string): Promise { return unwrapOrSelf(res.data); } +/** 微信网页授权登录(H5 点按钮,无需扫码) */ +export async function wxOAuthLogin(code: string): Promise { + const res = await axiosInstance.post('/rbac/auth/wx-oauth-login', { code }); + return unwrapOrSelf(res.data); +} + /** OAuth code 换取微信用户信息 */ export async function getOAuthUserInfo(code: string): Promise { const res = await axiosInstance.post(omsUrl('/wechat/oauth/user-info'), { code }); diff --git a/src/components/NanaAssistant/ai-advisor.css b/src/components/NanaAssistant/ai-advisor.css index 0f9b725..6b017fb 100644 --- a/src/components/NanaAssistant/ai-advisor.css +++ b/src/components/NanaAssistant/ai-advisor.css @@ -955,7 +955,7 @@ .ai-msg-row--assistant .ai-msg-bubble { background: #fff; - color: var(--kc-primary, #1b2a6b); + color: #1d2129; border: 1px solid rgba(27, 42, 107, 0.08); box-shadow: 0 2px 8px rgba(27, 42, 107, 0.04); border-bottom-left-radius: 6px; @@ -1055,7 +1055,7 @@ padding: 11px 4px 11px 12px; border: none; background: transparent; - color: var(--kc-primary, #1b2a6b); + color: #1d2129; /* ≥16px:避免 iOS/微信聚焦输入框时整页缩放 */ font-size: 16px; line-height: 1.45; @@ -1069,7 +1069,7 @@ } .ai-composer-input::placeholder { - color: rgba(106, 117, 184, 0.7); + color: #c9cdd4; } .ai-composer-send { @@ -1121,8 +1121,9 @@ .ai-composer-hint { margin: 0; font-size: 0.72rem; - color: var(--kc-secondary, #6a75b8); - opacity: 0.85; + color: #86909c; + opacity: 0.72; + font-weight: 400; } .ai-composer-human { diff --git a/src/utils/wechatOAuth.ts b/src/utils/wechatOAuth.ts index 64f85a8..4455072 100644 --- a/src/utils/wechatOAuth.ts +++ b/src/utils/wechatOAuth.ts @@ -6,7 +6,7 @@ import { stashInviteRef, } from '@/utils/inviteRefStorage'; -export type WeChatH5Page = 'register' | 'bind'; +export type WeChatH5Page = 'register' | 'bind' | 'login'; const DEFAULT_MP_APP_ID = 'wxd5141ba46c856e1f'; @@ -59,14 +59,37 @@ export function resolveWeChatInviteRef(query: Record): string { return readStashedInviteRef(); } -/** 跳转微信 OAuth;邀请码写入 state + 7 天暂存,回调后带回 */ -export function redirectToWeChatOAuth(page: WeChatH5Page, inviteCode?: string | null): void { - const appId = resolveWeChatMpAppId(); - const code = String(inviteCode ?? '').trim() || readStashedInviteRef(); - if (code) stashInviteRef(code); +function resolveOAuthRedirectPath(page: WeChatH5Page): string { + if (page === 'login') { + return `${window.location.origin}/h5/login`; + } + return `${window.location.origin}/wechat/${page}`; +} - const redirectUri = encodeURIComponent(`${window.location.origin}/wechat/${page}`); - const state = code ? `${page}:${encodeURIComponent(code)}` : page; +/** + * 跳转微信 OAuth。 + * - register/bind:邀请码写入 state + 7 天暂存 + * - login:可选把登录后回跳路径写入 state(login:/h5/orders) + */ +export function redirectToWeChatOAuth( + page: WeChatH5Page, + inviteCode?: string | null, + options?: { afterLoginPath?: string | null }, +): void { + const appId = resolveWeChatMpAppId(); + const redirectUri = encodeURIComponent(resolveOAuthRedirectPath(page)); + + let state = page; + if (page === 'login') { + const after = String(options?.afterLoginPath ?? '').trim(); + if (after.startsWith('/h5')) { + state = `login:${encodeURIComponent(after)}`; + } + } else { + const code = String(inviteCode ?? '').trim() || readStashedInviteRef(); + if (code) stashInviteRef(code); + state = code ? `${page}:${encodeURIComponent(code)}` : page; + } window.location.replace( `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}` + @@ -74,3 +97,17 @@ export function redirectToWeChatOAuth(page: WeChatH5Page, inviteCode?: string | `&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`, ); } + +/** 从 OAuth state 解析登录后回跳路径 */ +export function parseAfterLoginPathFromOAuthState(state: unknown): string { + const raw = String(state ?? '').trim(); + const match = raw.match(/^login:(.+)$/i); + if (!match?.[1]) return ''; + try { + const path = decodeURIComponent(match[1]).trim(); + return path.startsWith('/h5') ? path : ''; + } catch { + const path = match[1].trim(); + return path.startsWith('/h5') ? path : ''; + } +} diff --git a/src/views/h5/customer-center/assistant/index.vue b/src/views/h5/customer-center/assistant/index.vue index 260e003..d1530f3 100644 --- a/src/views/h5/customer-center/assistant/index.vue +++ b/src/views/h5/customer-center/assistant/index.vue @@ -310,6 +310,16 @@ function clearPriceContext() { :deep(.ai-composer-hint) { font-size: 11px; + color: #86909c; + opacity: 0.72; + } + + :deep(.ai-msg-row--assistant .ai-msg-bubble) { + color: #1d2129; + } + + :deep(.ai-composer-input) { + color: #1d2129; } :deep(.ai-msg-bubble) { diff --git a/src/views/h5/customer-center/components/WechatOAuthLoginButton.vue b/src/views/h5/customer-center/components/WechatOAuthLoginButton.vue new file mode 100644 index 0000000..993536e --- /dev/null +++ b/src/views/h5/customer-center/components/WechatOAuthLoginButton.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/views/h5/customer-center/locale/en-US.ts b/src/views/h5/customer-center/locale/en-US.ts index 1268916..27d15ec 100644 --- a/src/views/h5/customer-center/locale/en-US.ts +++ b/src/views/h5/customer-center/locale/en-US.ts @@ -144,12 +144,12 @@ export default { 'h5.me.guestTitle': 'Welcome to Customer Center', 'h5.me.guestDesc': - 'Long-press the QR code in WeChat to sign in. New users can register or bind first.', + 'Tap authorize in WeChat to sign in. New users can register or bind first.', 'h5.me.register': 'WeChat register', 'h5.me.bind': 'Bind existing account', 'h5.me.passwordLogin': 'Password login', 'h5.me.needLogin': 'Please register or sign in first', - 'h5.me.scanLogin': 'WeChat QR sign-in', + 'h5.me.scanLogin': 'WeChat authorize sign-in', 'h5.wxQr.loading': 'Loading QR code…', 'h5.wxQr.loadFailed': 'Failed to load QR code. Please refresh.', @@ -164,10 +164,15 @@ export default { 'h5.login.title': 'Sign in', 'h5.login.brand': 'Customer Center', - 'h5.login.tab.wechat': 'WeChat QR', + 'h5.login.tab.wechat': 'WeChat', 'h5.login.tab.password': 'Account', 'h5.login.switchPassword': 'Sign in with password', - 'h5.login.switchWechat': 'WeChat QR sign-in', + 'h5.login.switchWechat': 'WeChat authorize sign-in', + 'h5.login.oauthLogin': 'Sign in with WeChat', + 'h5.login.oauthRedirecting': 'Opening WeChat authorization…', + 'h5.login.oauthLoggingIn': 'Authorized. Signing you in…', + 'h5.login.oauthFailed': 'WeChat sign-in failed. Please retry.', + 'h5.login.openInWechat': 'Open this page in WeChat, then tap the button below to authorize.', 'h5.login.accountPlaceholder': 'Phone / email / customer code', 'h5.login.passwordPlaceholder': 'Enter password', 'h5.login.accountRequired': 'Please enter account', @@ -181,9 +186,9 @@ export default { 'h5.login.submitting': 'Signing in...', 'h5.login.success': 'Signed in', 'h5.login.failed': 'Sign-in failed. Check your account and password.', - 'h5.login.newUserHint': 'New here? Register or bind an account first', - 'h5.login.goRegister': 'Register', - 'h5.login.goBind': 'Bind account', + 'h5.login.newUserHint': 'No account? Authorize in WeChat to register or bind', + 'h5.login.goRegister': 'Authorize & register', + 'h5.login.goBind': 'Authorize & bind', 'h5.recharge.title': 'Recharge', 'h5.recharge.amount': 'Amount', @@ -537,7 +542,9 @@ export default { 'h5.me.editProfile': 'Edit profile', 'h5.me.addressManage': 'Addresses', 'h5.me.resetPassword': 'Change password', - 'h5.me.language': 'Language', + 'h5.me.language': 'Switch language', + 'h5.me.followOfficial': 'Follow official account', + 'h5.me.followOfficialMissing': 'Official account link is not configured. Please contact support.', 'h5.me.logout': 'Sign out', 'h5.me.logoutConfirm': 'Sign out now?', 'h5.me.customerCode': 'Code', diff --git a/src/views/h5/customer-center/locale/zh-CN.ts b/src/views/h5/customer-center/locale/zh-CN.ts index 842516a..a43108b 100644 --- a/src/views/h5/customer-center/locale/zh-CN.ts +++ b/src/views/h5/customer-center/locale/zh-CN.ts @@ -143,12 +143,12 @@ export default { 'h5.orders.detail.predeclareFailed': '预报失败', 'h5.me.guestTitle': '欢迎使用客户中心', - 'h5.me.guestDesc': '微信内长按二维码识别即可登录;新用户可先注册或绑定', + 'h5.me.guestDesc': '微信内点击授权即可登录;新用户请先注册或绑定', 'h5.me.register': '微信注册', 'h5.me.bind': '绑定已有账号', 'h5.me.passwordLogin': '账号密码登录', 'h5.me.needLogin': '请先注册或登录后使用', - 'h5.me.scanLogin': '微信扫码登录', + 'h5.me.scanLogin': '微信授权登录', 'h5.wxQr.loading': '二维码加载中…', 'h5.wxQr.loadFailed': '二维码加载失败,请刷新重试', @@ -163,10 +163,15 @@ export default { 'h5.login.title': '登录', 'h5.login.brand': '客户中心', - 'h5.login.tab.wechat': '微信扫码', + 'h5.login.tab.wechat': '微信登录', 'h5.login.tab.password': '账号登录', 'h5.login.switchPassword': '账号密码登录', - 'h5.login.switchWechat': '微信扫码登录', + 'h5.login.switchWechat': '微信授权登录', + 'h5.login.oauthLogin': '微信授权登录', + 'h5.login.oauthRedirecting': '正在打开微信授权…', + 'h5.login.oauthLoggingIn': '授权成功,正在登录…', + 'h5.login.oauthFailed': '微信授权登录失败,请重试', + 'h5.login.openInWechat': '请在微信内打开本页,点击下方按钮一键授权登录', 'h5.login.accountPlaceholder': '手机号 / 邮箱 / 客户编号', 'h5.login.passwordPlaceholder': '请输入密码', 'h5.login.accountRequired': '请输入账号', @@ -180,9 +185,9 @@ export default { 'h5.login.submitting': '登录中...', 'h5.login.success': '登录成功', 'h5.login.failed': '登录失败,请检查账号密码', - 'h5.login.newUserHint': '还没有账号?新客户请先注册或绑定', - 'h5.login.goRegister': '注册新账号', - 'h5.login.goBind': '绑定已有账号', + 'h5.login.newUserHint': '还没有账号?微信内可一键授权注册或绑定', + 'h5.login.goRegister': '微信授权注册', + 'h5.login.goBind': '微信授权绑定', 'h5.recharge.title': '账户充值', 'h5.recharge.amount': '充值金额', @@ -535,7 +540,9 @@ export default { 'h5.me.editProfile': '编辑资料', 'h5.me.addressManage': '地址管理', 'h5.me.resetPassword': '修改密码', - 'h5.me.language': '语言', + 'h5.me.language': '切换语言', + 'h5.me.followOfficial': '关注公众号', + 'h5.me.followOfficialMissing': '暂未配置公众号跳转链接,请联系客服', 'h5.me.logout': '退出登录', 'h5.me.logoutConfirm': '确定退出登录?', 'h5.me.customerCode': '编号', diff --git a/src/views/h5/customer-center/login/index.vue b/src/views/h5/customer-center/login/index.vue index b00604c..2643dc7 100644 --- a/src/views/h5/customer-center/login/index.vue +++ b/src/views/h5/customer-center/login/index.vue @@ -44,11 +44,9 @@ :class="{ 'is-active': activeTab === 'wechat' }" :aria-hidden="activeTab !== 'wechat'" > - + + +
('wechat'); const form = reactive({ account: '', password: '' }); const agreementChecked = ref(false); const loading = ref(false); +const oauthLoading = ref(false); const errorBox = ref(''); const agreementVisible = ref(false); const agreementTitle = ref(''); @@ -196,10 +200,18 @@ function goBack() { } function goRegister() { + if (isWeChatBrowser()) { + redirectToWeChatOAuth('register'); + return; + } h5Navigate(router, '/wechat/register'); } function goBind() { + if (isWeChatBrowser()) { + redirectToWeChatOAuth('bind'); + return; + } h5Navigate(router, '/wechat/bind'); } @@ -215,6 +227,8 @@ function openAgreement(type: 'privacy' | 'terms') { } function resolveRedirectPath() { + const fromState = parseAfterLoginPathFromOAuthState(route.query.state); + if (fromState) return fromState; const redirect = route.query.redirect; if (typeof redirect === 'string' && redirect.startsWith('/h5')) { return redirect; @@ -222,6 +236,11 @@ function resolveRedirectPath() { return '/h5/orders'; } +function stripOAuthQuery() { + const { code: _c, state: _s, ...rest } = route.query; + return rest; +} + async function finishLogin() { if (!rbacUserStore.userInfo) return; if (rbacUserStore.token) { @@ -232,21 +251,35 @@ async function finishLogin() { await nextTick(); const target = resolveRedirectPath(); try { - await router.replace(target); + await router.replace({ path: target, query: {} }); } catch (navErr) { console.error('H5 登录跳转失败', navErr); await router.replace('/h5/orders').catch(() => undefined); } } -async function onWxLoginSuccess(result: LoginResultDto) { - const ok = await rbacUserStore.loginByWxResult(result); - if (!ok) { - Message.error(t('h5.login.failed')); - return; +async function consumeOAuthCode(code: string) { + if (oauthLoading.value) return; + oauthLoading.value = true; + activeTab.value = 'wechat'; + try { + const result = await wxOAuthLogin(code); + const ok = await rbacUserStore.loginByWxResult(result); + if (!ok) { + Message.error(t('h5.login.failed')); + await router.replace({ path: route.path, query: stripOAuthQuery() }); + return; + } + Message.success(t('h5.login.success')); + await finishLogin(); + } catch (e: any) { + const msg = e?.message || t('h5.login.oauthFailed'); + Message.error(msg); + errorBox.value = msg; + await router.replace({ path: route.path, query: stripOAuthQuery() }); + } finally { + oauthLoading.value = false; } - Message.success(t('h5.wxQr.success')); - await finishLogin(); } async function handleSubmit() { @@ -292,7 +325,6 @@ async function handleSubmit() { watch( () => route.query.tab, (tab) => { - // 仅显式 ?tab=password 时进账号登录,其余一律默认微信扫码 if (tab === 'password') { activeTab.value = 'password'; errorBox.value = ''; @@ -304,9 +336,21 @@ watch( { immediate: true }, ); -onMounted(() => { +onMounted(async () => { if (isLogin()) { router.replace(resolveRedirectPath()); + return; + } + + const code = typeof route.query.code === 'string' ? route.query.code.trim() : ''; + if (code) { + await consumeOAuthCode(code); + return; + } + + // 微信内且未带 code:可一键拉起授权(用户点按钮亦可) + if (isWeChatBrowser() && route.query.auto === '1') { + redirectToWeChatOAuth('login', null, { afterLoginPath: resolveRedirectPath() }); } }); @@ -531,6 +575,13 @@ onMounted(() => { animation: h5-login-rise 0.28s var(--login-ease) both; } +.h5-login-oauth-status { + margin: 12px 0 0; + text-align: center; + font-size: 13px; + color: #86909c; +} + .h5-login-agree { margin: 4px 0 0; align-items: flex-start; diff --git a/src/views/h5/customer-center/me/index.vue b/src/views/h5/customer-center/me/index.vue index b320e9e..aaabe03 100644 --- a/src/views/h5/customer-center/me/index.vue +++ b/src/views/h5/customer-center/me/index.vue @@ -1,6 +1,6 @@