feat(client): 授权管理独立一级屏——授权/购买续费/订单管理三 tab(桌面+移动)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-11 00:24:59 +08:00
parent 9fe1e2766f
commit 22e07613a1
60 changed files with 1598 additions and 96 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 242 KiB

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/core/utils/clock.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/screens/license/license_screen.dart';
import '../support/shell_harness.dart';
/// 授权管理独立一级屏 golden × 三主题(回归闸 + 保真基准),照原型
/// design/prototype/screens/license.html:默认落「授权信息」tab(与原型截图
/// 默认态一致),三 tab 均可见(admin + 非 iOS)。到期日照抄 about/settings
/// 现有 golden 基准 2027-03-15appClock 冻结 2026-07-13 09:30:15。
/// node tools/fidelity.mjs license
/// 窄屏另见 license_mobile_golden_test.dart。
/// 更新基准:flutter test --update-goldens test/golden/license_golden_test.dart
class _FakeLicenseActive extends LicenseNotifier {
@override
Future<LicenseInfo?> build() async => LicenseInfo(
id: 1,
type: 'annual',
isActive: true,
maxDevices: 3,
phase: 'normal',
expiresAt: DateTime(2027, 3, 15),
);
}
List<Override> _overrides() => [
licenseProvider.overrideWith(() => _FakeLicenseActive()),
isReadonlyProvider.overrideWithValue(false),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
appClock = () => DateTime(2026, 7, 13, 9, 30, 15);
shellGoldenAcrossThemes(
'license 授权信息 tab 整框(桌面)',
path: '/license',
screen: (_) => const LicenseScreen(),
goldenPrefix: 'license',
overrides: _overrides,
logical: const Size(1280, 900),
);
}
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/core/utils/clock.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/screens/license/license_screen.dart';
import '../support/golden_harness.dart';
/// 授权管理独立一级屏窄屏 golden × 三主题(390×844 @2x),对齐原型
/// m-license.html:通栏 DsSeg 三 tab + 默认「授权信息」tab 内容。
/// 更新基准:flutter test --update-goldens test/golden/license_mobile_golden_test.dart
class _FakeAuth extends AuthNotifier {
_FakeAuth() {
state = const AuthState(
user: AuthUser(
accessToken: 't',
refreshToken: 'r',
id: 1,
username: 'wang',
realName: '王经理',
shopNo: 'DSJH-001',
shopId: 1,
role: 'admin'),
);
}
}
class _FakeLicenseActive extends LicenseNotifier {
@override
Future<LicenseInfo?> build() async => LicenseInfo(
id: 1,
type: 'annual',
isActive: true,
maxDevices: 3,
phase: 'normal',
expiresAt: DateTime(2027, 3, 15),
);
}
List<Override> _overrides() => [
authStateProvider.overrideWith((ref) => _FakeAuth()),
licenseProvider.overrideWith(() => _FakeLicenseActive()),
isReadonlyProvider.overrideWithValue(false),
];
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
appClock = () => DateTime(2026, 7, 13, 9, 30, 15);
goldenAcrossThemes(
'license 授权信息 tab 整屏(移动)',
goldenPrefix: 'license_mobile',
child: () => const Scaffold(body: LicenseScreen()),
overrides: _overrides,
logical: const Size(390, 844),
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

+132
View File
@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/core/theme/themes.dart';
import 'package:jiu_client/models/license.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/providers/license_purchase_provider.dart';
import 'package:jiu_client/screens/license/license_screen.dart';
/// 订单管理 tab 冒烟测试:切到该 tab 能正常渲染 KPI/表格,点击行能打开详情
/// 抽屉且不抛异常(此 tab 无 golden 覆盖,靠本测试兜底基础可用性)。
class _FakeAuth extends AuthNotifier {
_FakeAuth() {
state = const AuthState(
user: AuthUser(
accessToken: 't',
refreshToken: 'r',
id: 1,
username: 'wang',
realName: '王经理',
shopNo: 'S001',
shopId: 1,
role: 'admin'),
);
}
}
class _FakeLicense extends LicenseNotifier {
@override
Future<LicenseInfo?> build() async => LicenseInfo(
id: 1,
type: 'annual',
isActive: true,
maxDevices: 2,
phase: 'normal',
expiresAt: DateTime.now().add(const Duration(days: 100)),
);
}
final _records = [
PurchaseRecord(
outTradeNo: 'pay-20260710101433-9c41f7ae',
productBizCode: 'annual_pro',
amountMinor: 599900,
currency: 'CNY',
amount: '',
status: 'pending',
payUrl: 'https://pay.example.com/checkout/1',
userName: '王老板',
createdAt: DateTime(2026, 7, 10, 10, 14),
),
PurchaseRecord(
outTradeNo: 'pay-20260610093015-4d7a20cf',
productBizCode: 'monthly_pro',
amountMinor: 59900,
currency: 'CNY',
amount: '',
status: 'paid',
payUrl: '',
userName: '王老板',
createdAt: DateTime(2026, 6, 10, 9, 30),
paidAt: DateTime(2026, 6, 10, 9, 31),
renewedTo: DateTime(2027, 6, 4),
),
PurchaseRecord(
outTradeNo: 'pay-20260702174902-b2e85c10',
productBizCode: 'annual_pro',
amountMinor: 599900,
currency: 'CNY',
amount: '',
status: 'failed',
payUrl: '',
userName: '王老板',
createdAt: DateTime(2026, 7, 2, 17, 49),
),
];
class _FakePurchaseList extends PurchaseListNotifier {
@override
Future<PurchaseListResult> build() async => PurchaseListResult(
items: _records,
total: _records.length,
summary: const PurchaseSummary(
paidTotalMinor: 419700,
paidCount: 5,
pendingCount: 1,
totalCount: 7,
),
);
}
Widget _screen() => ProviderScope(
overrides: [
authStateProvider.overrideWith((ref) => _FakeAuth()),
licenseProvider.overrideWith(() => _FakeLicense()),
purchaseListProvider.overrideWith(() => _FakePurchaseList()),
],
child: MaterialApp(
theme: buildTheme('a'),
home: const Scaffold(body: LicenseScreen()),
),
);
void main() {
testWidgets('订单管理 tab:KPI + 表格 + 详情抽屉均正常渲染', (tester) async {
await tester.pumpWidget(_screen());
await tester.pumpAndSettle();
await tester.tap(find.text('订单管理'));
await tester.pumpAndSettle();
// KPI
expect(find.text('订单总数'), findsOneWidget);
expect(find.text('待支付 · 点击筛选'), findsOneWidget);
// 表格行(套餐 + 状态徽章)
expect(find.text('高级版 · 年付'), findsWidgets);
expect(find.text('高级版 · 月付'), findsOneWidget);
expect(find.text('待支付'), findsWidgets);
expect(find.text('已支付'), findsWidgets);
expect(find.text('已关闭'), findsWidgets);
// 点开第一行详情(待支付单):抽屉出现「继续支付」「取消订单」
await tester.tap(find.text('pay-20260710101433-9c41f7ae'));
await tester.pumpAndSettle();
expect(find.text('订单详情'), findsOneWidget);
expect(find.text('继续支付'), findsOneWidget);
expect(find.text('取消订单'), findsOneWidget);
});
}
+5 -8
View File
@@ -7,8 +7,9 @@ import 'package:jiu_client/core/theme/themes.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/screens/settings/license_panel.dart';
/// iOS App Store 合规形态(苹果 3.1.1):授权管理页隐藏一切外部购买 UI
/// 补位中性「联系客服」卡;其余平台购买卡照旧、无客服卡。
/// iOS App Store 合规形态(苹果 3.1.1):LicensePanel(授权信息 tab 内容
/// 2026-07-10 起「在线购买续费」已独立为 LicenseScreen 的 tab2,见
/// license_screen_tabs_test.dartiOS 态补位中性「联系客服」卡,其余平台无。
class _FakeAuth extends AuthNotifier {
_FakeAuth(String role) {
@@ -54,24 +55,20 @@ Widget _panel({required String role}) => ProviderScope(
void main() {
tearDown(() => debugForceHideExternalPurchaseUi = null);
testWidgets('iOS 形态:购买卡与价格链接消失,出现联系客服卡', (tester) async {
testWidgets('iOS 形态:出现联系客服卡', (tester) async {
debugForceHideExternalPurchaseUi = true;
await tester.pumpWidget(_panel(role: 'operator'));
await tester.pumpAndSettle();
expect(find.text('在线购买 / 续费'), findsNothing);
expect(find.textContaining('查看套餐价格'), findsNothing);
expect(find.text('联系客服'), findsOneWidget);
expect(find.text(supportEmail), findsOneWidget);
});
testWidgets('非 iOS购买卡照旧,无联系客服卡', (tester) async {
testWidgets('非 iOS:无联系客服卡', (tester) async {
debugForceHideExternalPurchaseUi = false;
await tester.pumpWidget(_panel(role: 'operator'));
await tester.pumpAndSettle();
expect(find.text('在线购买 / 续费'), findsOneWidget);
expect(find.textContaining('查看套餐价格'), findsOneWidget);
expect(find.text('联系客服'), findsNothing);
});
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/core/config/store_compliance.dart';
import 'package:jiu_client/core/theme/themes.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/screens/license/license_screen.dart';
/// 授权管理独立一级屏(2026-07-10 三 tab 化)tab 可见性回归:
/// - 「在线购买续费」iOS App Store 合规态(3.1.1)整 tab 隐藏;
/// - 「订单管理」GET /license/purchases 仅管理员可用,非管理员隐藏该 tab。
class _FakeAuth extends AuthNotifier {
_FakeAuth(String role) {
state = AuthState(
user: AuthUser(
accessToken: 't',
refreshToken: 'r',
id: 1,
username: 'wang',
realName: '王经理',
shopNo: 'S001',
shopId: 1,
role: role),
);
}
}
class _FakeLicense extends LicenseNotifier {
@override
Future<LicenseInfo?> build() async => LicenseInfo(
id: 1,
type: 'annual',
isActive: true,
maxDevices: 2,
phase: 'normal',
expiresAt: DateTime.now().add(const Duration(days: 100)),
);
}
Widget _screen({required String role}) => ProviderScope(
overrides: [
authStateProvider.overrideWith((ref) => _FakeAuth(role)),
licenseProvider.overrideWith(() => _FakeLicense()),
],
child: MaterialApp(
theme: buildTheme('a'),
home: const Scaffold(body: LicenseScreen()),
),
);
void main() {
tearDown(() => debugForceHideExternalPurchaseUi = null);
testWidgets('管理员 + 非 iOS:三 tab 全部可见', (tester) async {
debugForceHideExternalPurchaseUi = false;
await tester.pumpWidget(_screen(role: 'admin'));
await tester.pumpAndSettle();
expect(find.text('在线购买 / 续费'), findsOneWidget);
expect(find.text('订单管理'), findsOneWidget);
});
testWidgets('iOS 合规态:购买 tab 隐藏,订单 tab 仍在', (tester) async {
debugForceHideExternalPurchaseUi = true;
await tester.pumpWidget(_screen(role: 'admin'));
await tester.pumpAndSettle();
expect(find.text('在线购买 / 续费'), findsNothing);
expect(find.text('订单管理'), findsOneWidget);
});
testWidgets('非管理员:订单管理 tab 隐藏,购买 tab 仍可见', (tester) async {
debugForceHideExternalPurchaseUi = false;
await tester.pumpWidget(_screen(role: 'operator'));
await tester.pumpAndSettle();
expect(find.text('在线购买 / 续费'), findsOneWidget);
expect(find.text('订单管理'), findsNothing);
});
}
+4 -1
View File
@@ -67,7 +67,8 @@ List<Override> shellOverrides() => [
isReadonlyProvider.overrideWithValue(false),
];
// 外壳 nav 引用 9 个分支序号(入/出库·库存·财务·往来·基础数据·设备·设置·关于)。
// 外壳 nav 引用的分支序号需与 app_router.dart 一一对齐(0-8 侧栏 + 9 我的
// 占位 + 10 授权管理),否则侧栏高亮(_navGroups item.index)会错位。
StatefulShellBranch _branch(String path, [WidgetBuilder? builder]) =>
StatefulShellBranch(routes: [
GoRoute(
@@ -92,6 +93,8 @@ GoRouter _router(String initialPath, WidgetBuilder screen) => GoRouter(
'/devices',
'/settings',
'/about',
'/me', // 9:桌面侧栏不含此项,占位保持分支序号对齐
'/license', // 10:授权管理独立一级屏
])
_branch(p, p == initialPath ? screen : null),
],