feat(client): 授权管理独立一级屏——授权/购买续费/订单管理三 tab(桌面+移动)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Before Width: | Height: | Size: 477 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 480 KiB After Width: | Height: | Size: 483 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 420 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 420 KiB After Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 427 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 342 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 296 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 237 KiB |
|
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-15,appClock 冻结 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),
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 276 KiB |
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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.dart)iOS 态补位中性「联系客服」卡,其余平台无。
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
|
||||