diff --git a/client/lib/screens/account_page.dart b/client/lib/screens/account_page.dart index 3f6a775..3930a0b 100644 --- a/client/lib/screens/account_page.dart +++ b/client/lib/screens/account_page.dart @@ -60,15 +60,6 @@ class AccountPage extends ConsumerWidget { isFree: isFree, email: email, expiresLabel: expiresLabel, - // Renew/Upgrade 直达购买页(套餐 + 渠道 switch),不再经 PlansScreen。 - onUpgrade: () => open( - NavView.purchase, - PurchaseScreen( - t: t, - onOrderCreated: () => Navigator.of(context).push(MaterialPageRoute( - builder: (_) => PaymentScreen(t: t, onDone: () => Navigator.of(context).pop()))), - ), - ), ), const SizedBox(height: 18), // 账户信息 @@ -156,13 +147,11 @@ class _PlanBanner extends StatelessWidget { required this.isFree, required this.email, required this.expiresLabel, - required this.onUpgrade, }); final AppText t; final bool isFree; final String email; final String expiresLabel; - final VoidCallback onUpgrade; @override Widget build(BuildContext context) { @@ -193,7 +182,6 @@ class _PlanBanner extends StatelessWidget { child: Text(t.freePlanName, style: PangolinText.caption.copyWith(color: c.fg2, fontWeight: FontWeight.w600, fontSize: 11)), ), ])), - _UpgradeButton(label: t.upgradeBtn, onTap: onUpgrade), ]), ]), ); @@ -222,40 +210,11 @@ class _PlanBanner extends StatelessWidget { Text(expiresLabel.isEmpty ? t.proMember : '${t.proMember} · $expiresLabel', style: TextStyle(color: PangolinColors.white.withValues(alpha: 0.85), fontSize: 12)), ])), - FilledButton( - onPressed: onUpgrade, - style: FilledButton.styleFrom( - backgroundColor: PangolinColors.white, - foregroundColor: PangolinColors.clay700, - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - ), - child: Text(t.upgradeBtn, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), - ), ]), ); } } -class _UpgradeButton extends StatelessWidget { - const _UpgradeButton({required this.label, required this.onTap}); - final String label; - final VoidCallback onTap; - @override - Widget build(BuildContext context) { - final c = context.pangolin; - return FilledButton( - onPressed: onTap, - style: FilledButton.styleFrom( - backgroundColor: c.accent, - foregroundColor: c.fgOnAccent, - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - ), - child: Text(label, style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), - ); - } -} class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.text}); diff --git a/client/lib/state/app_providers.dart b/client/lib/state/app_providers.dart index 673d8c1..bb63ece 100644 --- a/client/lib/state/app_providers.dart +++ b/client/lib/state/app_providers.dart @@ -1,4 +1,6 @@ // app_providers.dart — 语言 / 主题 / 套餐视角等基础状态(Riverpod) +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -32,15 +34,29 @@ AppText appTextFor(AppLang lang) { } } -/// 当前语言(单显)。默认英文(国际化默认语种);用户选择持久化到 -/// shared_preferences(key `pg_lang`,存枚举 name),重启保留 —— 原来无持久化, -/// 切了语言重启会丢。设置/账户页经 `.notifier).set(lang)` 切换。 +/// 当前语言。首启无用户选择时**跟随系统语言**(设备 locale → 支持的 AppLang, +/// 命不中回退英文);用户在设置/账户页显式切换后持久化到 shared_preferences +/// (key `pg_lang`,存枚举 name),此后一律以用户选择为准、不再看系统。 class LocaleNotifier extends StateNotifier { - LocaleNotifier() : super(AppLang.en) { + LocaleNotifier() : super(_systemDefault()) { _load(); } static const _key = 'pg_lang'; + /// 设备语言 → 支持的 AppLang。取平台首选 locale 的语言码(zh/ja/ko/ru/es 命中 + /// 对应语种,其余一律英文)。构造期同步可用,无需 await。 + static AppLang _systemDefault() { + try { + final code = ui.PlatformDispatcher.instance.locale.languageCode.toLowerCase(); + for (final l in AppLang.values) { + if (l.name == code) return l; + } + } catch (_) { + /* 取系统 locale 失败 → 英文 */ + } + return AppLang.en; + } + Future _load() async { try { final saved = (await SharedPreferences.getInstance()).getString(_key); @@ -53,7 +69,7 @@ class LocaleNotifier extends StateNotifier { } } } catch (_) { - /* 读失败保持默认 en */ + /* 读失败保持系统默认 */ } } diff --git a/web/usercenter/app/sso/page.tsx b/web/usercenter/app/sso/page.tsx index 61fdb05..7b2beee 100644 --- a/web/usercenter/app/sso/page.tsx +++ b/web/usercenter/app/sso/page.tsx @@ -25,6 +25,17 @@ import { makeT } from '../../lib/i18n'; import { getClient } from '../../lib/api/client'; import { bilingual } from '../../lib/api/errors'; +/** 站点 basePath(与 next.config.js 一致)。本页用的是 window.location 原生跳转, + * Next 不会自动补 basePath,故绝对路径必须手动加前缀,否则会落到站点根(官网主页) + * 而不是用户中心 —— 这正是「点用户中心却跳到主页、还要求重新登录」的根因。 */ +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '/user'; + +/** 把本站相对路径补上 basePath(供 window.location 原生跳转用)。 */ +function withBase(path: string): string { + const p = path.startsWith('/') ? path : `/${path}`; + return `${BASE_PATH}${p}`; +} + /** redirect 白名单:仅本站相对路径(单个 '/' 开头),其余一律回 '/'。 */ function safeRedirect(raw: string | null): string { if (!raw) return '/'; @@ -45,14 +56,15 @@ export default function SsoPage() { const redirect = safeRedirect(params.get('redirect')); // 票据只用一次,立刻从地址栏抹掉,不留浏览器历史(与 jiu 一致)。 + // 原生 history/location 不吃 Next 的 basePath,须手动补,否则地址与跳转都会漂到站点根。 try { - window.history.replaceState(null, '', '/sso/'); + window.history.replaceState(null, '', withBase('/sso/')); } catch { /* ignore */ } if (!ticket) { - window.location.replace('/'); + window.location.replace(withBase('/')); return; } @@ -61,14 +73,14 @@ export default function SsoPage() { .exchangeWebTicket(ticket) .then(() => { if (!alive) return; - window.location.replace(redirect); + window.location.replace(withBase(redirect)); }) .catch((e) => { if (!alive) return; setMsg(bilingual(e, lang)); setFailed(true); setTimeout(() => { - window.location.replace('/'); + window.location.replace(withBase('/')); }, 1500); });