From a02d7a5e981161732c6195bd8243cdcdad176acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 29 Sep 2026 11:04:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E6=B5=B7=E6=8A=A5=E5=BC=B9?= =?UTF-8?q?=E5=B1=82=E5=86=85=E4=B8=AD/English=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E2=80=94=E2=80=94=E4=BA=8C=E7=BB=B4=E7=A0=81=E6=B5=B7=E6=8A=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=8B=B1=E6=96=87=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 弹层顶部 SegmentTabs(light) 两档,默认跟 App 语言前缀;只影响海报内容 与二维码 ?lang=,不动 App 语言设置 - 提示文案经 lookupAppLocalizations 按海报语言取(arb 中英双全,零硬编码) - 标题/场地/地址复用相册多语言回退链按海报语言取 - QrImageView 补 semanticsLabel(读屏原本无名,兼作测试断言口) Co-Authored-By: Claude Code --- apps/mobile/lib/pages/share_qr_dialog.dart | 36 +++++++++++++++--- .../mobile/test/pages/share_qr_page_test.dart | 38 ++++++++++++++++++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/apps/mobile/lib/pages/share_qr_dialog.dart b/apps/mobile/lib/pages/share_qr_dialog.dart index 4a12ec8..d18061f 100644 --- a/apps/mobile/lib/pages/share_qr_dialog.dart +++ b/apps/mobile/lib/pages/share_qr_dialog.dart @@ -27,6 +27,7 @@ import '../features/albums/album_format.dart'; import '../features/albums/models.dart'; import '../l10n/app_localizations.dart'; import '../ui/components/photo_thumbnail.dart'; +import '../ui/components/segment_tabs.dart'; import '../ui/tokens.dart'; /// 外链打开注入点(生产=url_launcher;测试 override)。 @@ -325,6 +326,9 @@ class ShareQrPage extends ConsumerWidget { /// 二维码海报弹层:宣传图(banner→cover 兜底)+ 标题 + 时间 + 地址 + 二维码; /// 「保存海报图」= RepaintBoundary 3x 渲染 PNG → 相册(gal)。 +/// +/// 弹层内「中文/English」切换海报语言(默认跟 App 语言):内容文案/相册 +/// 字段/二维码 `?lang=` 随切换,App 语言设置与弹层 chrome 不受影响。 class _PosterSheet extends ConsumerStatefulWidget { const _PosterSheet({required this.album}); @@ -339,6 +343,12 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> { String? _bannerUrl; bool _saving = false; + /// 海报语言('zh'|'en';打开时刻快照 App 语言前缀,切换只影响海报)。 + late String _posterLang = + AppLocalizations.of(context).localeName.toLowerCase().startsWith('zh') + ? 'zh' + : 'en'; + @override void initState() { super.initState(); @@ -377,9 +387,10 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); - final localeName = l10n.localeName; - final url = shareAlbumUrl(widget.album, localeName); - final title = albumDisplayTitle(widget.album, localeName); + // 海报内容按海报语言渲染(弹层 chrome 仍跟 App 语言)。 + final posterL10n = lookupAppLocalizations(Locale(_posterLang)); + final url = shareAlbumUrl(widget.album, _posterLang); + final title = albumDisplayTitle(widget.album, _posterLang); // 海报日期:日期粒度(UI06 选择器已去时间;展示同口径)。 String posterDate(DateTime utc) { final wall = tryToAlbumZone(utc, widget.album.timezone) ?? utc.toUtc(); @@ -391,9 +402,9 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> { ? null : posterDate(widget.album.endAt!); final dateRange = end == null ? start : '$start ~ $end'; - final venue = albumDisplayVenue(widget.album, localeName); + final venue = albumDisplayVenue(widget.album, _posterLang); String? addressForPoster(AlbumSummary album) { - for (final t in albumDisplayTranslations(album, localeName)) { + for (final t in albumDisplayTranslations(album, _posterLang)) { final a = t.address?.trim(); if (a != null && a.isNotEmpty) return a; } @@ -426,6 +437,18 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> { ), ], ), + const SizedBox(height: kSpace8), + // ── 海报语言切换:内容/二维码随之,App 语言不动 ── + SegmentTabs( + value: _posterLang, + onChanged: (lang) => setState(() => _posterLang = lang), + style: SegmentTabsStyle.light, + segments: [ + ('zh', l10n.localeChinese), + ('en', l10n.localeEnglish), + ], + ), + const SizedBox(height: kSpace12), // ── 海报(整块 RepaintBoundary;下载即渲染此区域)── RepaintBoundary( key: _posterKey, @@ -520,12 +543,13 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> { version: QrVersions.auto, size: 200, backgroundColor: Colors.white, + semanticsLabel: url, ), ), const SizedBox(height: kSpace12), Center( child: Text( - l10n.shareScanHint, + posterL10n.shareScanHint, style: theme.textTheme.bodySmall?.copyWith( color: const Color(0xFF69778E), ), diff --git a/apps/mobile/test/pages/share_qr_page_test.dart b/apps/mobile/test/pages/share_qr_page_test.dart index e0bc93d..58cfb8a 100644 --- a/apps/mobile/test/pages/share_qr_page_test.dart +++ b/apps/mobile/test/pages/share_qr_page_test.dart @@ -4,7 +4,8 @@ /// 可选中链接)、链接携带正确相册 token 与 `?lang=` 语言参数、Copy link /// 主操作写剪贴板 + 已复制提示、Open in browser 次操作经注入替身启动器 /// 打开并退出页面、未公开相册显示「该链接目前无法公开浏览」琥珀提示 -/// (公开相册不显示)。 +/// (公开相册不显示)、海报弹层内「中文/English」切换(默认跟 App 语言, +/// 提示文案/相册标题/二维码 lang 随切换,弹层 chrome 仍跟 App 语言)。 library; import 'package:flutter/material.dart'; @@ -85,6 +86,41 @@ void main() { expect(find.text('2026/9/18'), findsOneWidget); }); + testWidgets('海报语言切换:默认跟 App 语言,切中文后提示/标题/二维码随之', ( + tester, + ) async { + await pumpPage(tester); + await tester.tap(find.text('Generate QR poster')); + await tester.pumpAndSettle(); + + // 默认 English 档(App 语言 en):英文提示 + lang=en 的二维码 + // (qr_flutter 4.1 无公开 data getter,经 semanticsLabel 断言编码内容)。 + expect(find.text('Scan to view photos'), findsOneWidget); + expect( + tester.widget(find.byType(QrImageView)).semanticsLabel, + 'http://10.0.2.2:5175/a/$token?lang=en', + reason: '二维码 lang 跟海报语言(默认=App 语言前缀)', + ); + // 两档切换(语言名以自身语言书写,与语言选择面板同约定)。 + expect(find.text('中文'), findsOneWidget); + expect(find.text('English'), findsOneWidget); + // 英文档标题取英文翻译(中文翻译不出现在海报上)。 + expect(find.text('开幕晚宴'), findsNothing); + + // 切「中文」:提示/标题换中文、二维码 lang=zh-CN。 + await tester.tap(find.text('中文')); + await tester.pumpAndSettle(); + expect(find.text('扫码查看照片'), findsOneWidget); + expect(find.text('Scan to view photos'), findsNothing); + expect(find.text('开幕晚宴'), findsOneWidget); + expect( + tester.widget(find.byType(QrImageView)).semanticsLabel, + 'http://10.0.2.2:5175/a/$token?lang=zh-CN', + ); + // 弹层 chrome(标题/下载按钮)不随海报语言切换,仍跟 App 语言。 + expect(find.text('Download poster image'), findsOneWidget); + }); + testWidgets('Copy link(主操作)→ 剪贴板收到带 lang 的 URL + 已复制提示', (tester) async { final clipboardCalls = [];