feat(client): 在线购买改为设置页内联卡片,左侧菜单改「授权管理」

- purchase_dialog.dart → purchase_card.dart:弹窗改 PurchaseCard 内联卡片,
  三阶段(选套餐/待支付/成功)操作按钮改卡片内右对齐,新增「重新选择」回退
- 设置授权面板拆两张卡:授权管理(到期统计+兑换券+降级说明)+
  在线购买/续费(仅管理员可见,PurchaseCard 内联)
- 移除统计条与移动端的弹窗入口按钮

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 23:39:12 +08:00
parent 15ed05dc1e
commit 02db118f61
8 changed files with 186 additions and 189 deletions
+117 -118
View File
@@ -1,6 +1,6 @@
// screens/settings/settings_screen.dart — 系统设置(照原型 settings.html 重建)。
// 布局:.head + .settings grid(左 .subnav 200px + 右面板卡)。子导航 5 项 =
// 原型 4(门店信息/用户管理/授权兑换券/偏好设置)+ 保留真实功能「编号规则」。
// 原型 4(门店信息/用户管理/授权管理/偏好设置)+ 保留真实功能「编号规则」。
// 用户管理面板 = 预览表 + 「管理全部用户」→ /settings/users 独立页(原型 users.html)。
// 原「系统参数」假设置(本地 state 不落后端)已按用户口径删除。
import 'package:file_picker/file_picker.dart';
@@ -32,7 +32,7 @@ import '../../widgets/ds/ds_atoms.dart';
import '../../widgets/ds/ds_menu.dart';
import '../../widgets/ds/ds_table.dart';
import '../../widgets/write_guard.dart';
import 'purchase_dialog.dart';
import 'purchase_card.dart';
import 'users_screen.dart' show userRoleBadge, userStatusBadge;
import '../../core/theme/app_fonts.dart';
import '../../widgets/ds/ds_toast.dart';
@@ -53,7 +53,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
(LucideIcons.house, '门店信息'),
(LucideIcons.userPlus, '用户管理'),
(LucideIcons.hash, '编号规则'),
(LucideIcons.trendingUp, '授权兑换券'),
(LucideIcons.trendingUp, '授权管理'),
(LucideIcons.contrast, '偏好设置'),
];
late int _panel = widget.initialTab.clamp(0, _navs.length - 1);
@@ -641,7 +641,7 @@ class _StoreInfoPanelState extends ConsumerState<_StoreInfoPanel> {
}
}
// ── 面板 4:授权兑换券 ───────────────────────────────────────
// ── 面板 4:授权管理 ───────────────────────────────────────
class _LicensePanel extends ConsumerStatefulWidget {
const _LicensePanel();
@@ -696,132 +696,131 @@ class _LicensePanelState extends ConsumerState<_LicensePanel> {
? lic!.expiresAt!.difference(appNow()).inDays.clamp(0, 99999)
: null;
return _SettingsCard(
title: '授权管理',
desc: '在线购买或使用兑换券续期,时长在现有到期时间上叠加',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 原型 .lic 统计条:bg 底 / border / r-md / pad 18 20
// 主 CTA「在线购买 / 续费」宽屏并排在统计条内,窄屏换行到统计条下方通栏
Container(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 18),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
_stat(t, '授权到期日', expires),
const SizedBox(width: 24),
_stat(t, '剩余天数', days != null ? '剩余 $days' : '', warn: true),
const Spacer(),
if (isAdmin && !context.isMobile) ...[
DsButton('在线购买 / 续费',
variant: DsBtnVariant.primary,
onPressed: () => showPurchaseDialog(context)),
const SizedBox(width: 14),
],
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SettingsCard(
title: '授权管理',
desc: '使用兑换券续期,时长在现有到期时间上叠加',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 原型 .lic 统计条:bg 底 / border / r-md / pad 18 20
Container(
width: 46,
height: 46,
padding: const EdgeInsets.fromLTRB(20, 18, 20, 18),
decoration: BoxDecoration(
color: t.okSoft,
color: t.bg,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child:
Icon(LucideIcons.shieldCheck, size: 22, color: t.success),
child: Row(children: [
_stat(t, '授权到期日', expires),
const SizedBox(width: 24),
_stat(t, '剩余天数', days != null ? '剩余 $days' : '',
warn: true),
const Spacer(),
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: t.okSoft,
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Icon(LucideIcons.shieldCheck,
size: 22, color: t.success),
),
]),
),
]),
),
if (isAdmin && context.isMobile) ...[
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: DsButton('在线购买 / 续费',
variant: DsBtnVariant.primary,
onPressed: () => showPurchaseDialog(context)),
),
],
const SizedBox(height: 16),
// 兑换(原型 .redeeminput + 按钮,max 520
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('兑换券短码',
style: TextStyle(
fontSize: AppDims.fsSm, color: t.muted)),
const SizedBox(height: 6),
DsInput(
controller: _voucherCtrl,
hintText: '输入兑换券短码',
const SizedBox(height: 16),
// 兑换(原型 .redeeminput + 按钮,max 520
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('兑换券短码',
style: TextStyle(
fontSize: AppDims.fsSm, color: t.muted)),
const SizedBox(height: 6),
DsInput(
controller: _voucherCtrl,
hintText: '输入兑换券短码',
),
],
),
],
),
),
const SizedBox(width: 10),
WriteGuard(
child: DsButton(_redeeming ? '兑换中…' : '兑换续期',
variant: DsBtnVariant.primary,
onPressed: _redeeming ? null : _redeem),
),
],
),
const SizedBox(width: 10),
WriteGuard(
child: DsButton(_redeeming ? '兑换中…' : '兑换续期',
variant: DsBtnVariant.primary,
onPressed: _redeeming ? null : _redeem),
),
],
),
),
// 在线购买仅管理员(后端 purchase 接口 admin only,主 CTA 已上移到统计条);
// 其他角色引导看官网价格页
if (!isAdmin) ...[
const SizedBox(height: 14),
Row(children: [
Text('在线购买请联系门店管理员 · ',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
InkWell(
onTap: () =>
launchUrl(Uri.parse('${AppInfo.website}/#pricing')),
child: Text('查看套餐价格 →',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.primary)),
),
]),
],
const SizedBox(height: 16),
// 过期降级说明(现有能力保留)
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.borderSubtle),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('到期后的降级规则',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.heading)),
const SizedBox(height: 6),
for (final note in LicenseCopy.degradationNotes())
Padding(
padding: const EdgeInsets.only(top: 3),
child: Text('· $note',
style:
TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
// 在线购买仅管理员(后端 purchase 接口 admin only,主 CTA 已上移到统计条);
// 其他角色引导看官网价格页
if (!isAdmin) ...[
const SizedBox(height: 14),
Row(children: [
Text('在线购买请联系门店管理员 · ',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
InkWell(
onTap: () =>
launchUrl(Uri.parse('${AppInfo.website}/#pricing')),
child: Text('查看套餐价格 →',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.primary)),
),
]),
],
),
const SizedBox(height: 16),
// 过期降级说明(现有能力保留)
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.borderSubtle),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('到期后的降级规则',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.heading)),
const SizedBox(height: 6),
for (final note in LicenseCopy.degradationNotes())
Padding(
padding: const EdgeInsets.only(top: 3),
child: Text('· $note',
style: TextStyle(
fontSize: AppDims.fsSm, color: t.muted)),
),
],
),
),
],
),
),
// 在线购买仅管理员(后端 purchase 接口 admin only),单独一张卡内联展示
if (isAdmin) ...[
const SizedBox(height: 16),
const _SettingsCard(
title: '在线购买 / 续费',
desc: '选择套餐支付宝支付,到账后授权自动续期',
child: PurchaseCard(),
),
],
),
],
);
}