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
@@ -1,6 +1,6 @@
|
||||
// screens/settings/purchase_dialog.dart — App 内在线购买/续费授权。
|
||||
// 流程:套餐选择(标准/高级 × 月付/年付)→ POST /license/purchase 下单 →
|
||||
// 外部浏览器打开 pay 收银台(移动端由系统拉起支付宝 App)→ 本弹窗轮询
|
||||
// screens/settings/purchase_card.dart — App 内在线购买/续费授权(内联卡片)。
|
||||
// 流程:套餐选择(标准/首月特惠/高级,非特惠可切月付/年付)→ POST /license/purchase
|
||||
// 下单 → 外部浏览器打开 pay 收银台(移动端由系统拉起支付宝 App)→ 本卡片轮询
|
||||
// GET /license/purchase/:otn,webhook 续期到账后自动刷新授权并展示新到期日。
|
||||
// 价格仅展示,实付以 pay 侧套餐表为准(下单响应回传 amount)。
|
||||
import 'dart:async';
|
||||
@@ -12,35 +12,25 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../core/config/license_plans.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_dims.g.dart';
|
||||
import '../../core/theme/app_fonts.dart';
|
||||
import '../../core/theme/app_tokens.dart';
|
||||
import '../../core/theme/context_tokens.dart';
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import '../../models/license.dart';
|
||||
import '../../providers/license_provider.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_toast.dart';
|
||||
|
||||
Future<void> showPurchaseDialog(BuildContext context) {
|
||||
return showAppDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const _PurchaseDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
enum _Phase { pick, waiting, success }
|
||||
|
||||
class _PurchaseDialog extends ConsumerStatefulWidget {
|
||||
const _PurchaseDialog();
|
||||
/// 在线购买/续费卡片内容(由设置页包进 _SettingsCard,仅管理员可见)。
|
||||
class PurchaseCard extends ConsumerStatefulWidget {
|
||||
const PurchaseCard({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<_PurchaseDialog> createState() => _PurchaseDialogState();
|
||||
ConsumerState<PurchaseCard> createState() => _PurchaseCardState();
|
||||
}
|
||||
|
||||
class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
_Phase _phase = _Phase.pick;
|
||||
int _tabIdx = 0; // 0=标准版 1=首月特惠 2=高级版
|
||||
int _cycleIdx = 0; // 0=年付 1=月付
|
||||
@@ -77,8 +67,9 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
Future<void> _submit() async {
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
final order =
|
||||
await ref.read(licenseRepositoryProvider).createPurchase(_plan.bizCode);
|
||||
final order = await ref
|
||||
.read(licenseRepositoryProvider)
|
||||
.createPurchase(_plan.bizCode);
|
||||
// 外部浏览器打开收银台:桌面端进网页支付,移动端由系统浏览器拉起支付宝
|
||||
await launchUrl(Uri.parse(order.payUrl),
|
||||
mode: LaunchMode.externalApplication);
|
||||
@@ -88,8 +79,7 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
_phase = _Phase.waiting;
|
||||
_submitting = false;
|
||||
});
|
||||
_pollTimer =
|
||||
Timer.periodic(const Duration(seconds: 3), (_) => _poll());
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 3), (_) => _poll());
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _submitting = false);
|
||||
@@ -102,8 +92,9 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
if (order == null || _checking) return;
|
||||
_checking = true;
|
||||
try {
|
||||
final st =
|
||||
await ref.read(licenseRepositoryProvider).purchaseStatus(order.outTradeNo);
|
||||
final st = await ref
|
||||
.read(licenseRepositoryProvider)
|
||||
.purchaseStatus(order.outTradeNo);
|
||||
if (!mounted) return;
|
||||
if (st.isPaid) {
|
||||
_pollTimer?.cancel();
|
||||
@@ -124,49 +115,26 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 回到套餐选择(仅停止本地轮询,不影响已支付订单到账)。
|
||||
void _backToPick() {
|
||||
_pollTimer?.cancel();
|
||||
setState(() {
|
||||
_order = null;
|
||||
_newExpiresAt = null;
|
||||
_phase = _Phase.pick;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
return AlertDialog(
|
||||
title: Text(switch (_phase) {
|
||||
_Phase.pick => '在线购买 / 续费',
|
||||
_Phase.waiting => '等待支付',
|
||||
_Phase.success => '支付成功',
|
||||
}),
|
||||
content: SizedBox(
|
||||
width: context.dialogWidth(460),
|
||||
child: switch (_phase) {
|
||||
_Phase.pick => _buildPick(t),
|
||||
_Phase.waiting => _buildWaiting(t),
|
||||
_Phase.success => _buildSuccess(t),
|
||||
},
|
||||
),
|
||||
actions: switch (_phase) {
|
||||
_Phase.pick => [
|
||||
DsButton('取消',
|
||||
onPressed:
|
||||
_submitting ? null : () => Navigator.of(context).pop()),
|
||||
DsButton(
|
||||
_submitting
|
||||
? '正在创建订单…'
|
||||
: _isPromo && _promoUsed
|
||||
? '本店已享受过'
|
||||
: '提交订单 · ¥${_fmt(_plan.price)}',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed:
|
||||
_submitting || (_isPromo && _promoUsed) ? null : _submit),
|
||||
],
|
||||
_Phase.waiting => [
|
||||
DsButton('稍后再说', onPressed: () => Navigator.of(context).pop()),
|
||||
DsButton('我已完成支付',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => _poll(manual: true)),
|
||||
],
|
||||
_Phase.success => [
|
||||
DsButton('完成',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => Navigator.of(context).pop()),
|
||||
],
|
||||
// 与授权卡兑换区一致收窄内容宽度,避免宽屏卡片内元素拉伸过长
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: switch (_phase) {
|
||||
_Phase.pick => _buildPick(t),
|
||||
_Phase.waiting => _buildWaiting(t),
|
||||
_Phase.success => _buildSuccess(t),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -291,6 +259,19 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
const SizedBox(height: 6),
|
||||
Text('到期时间自动叠加,未到期续费不吃亏 · 支付宝支付',
|
||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: DsButton(
|
||||
_submitting
|
||||
? '正在创建订单…'
|
||||
: _isPromo && _promoUsed
|
||||
? '本店已享受过'
|
||||
: '提交订单 · ¥${_fmt(_plan.price)}',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed:
|
||||
_submitting || (_isPromo && _promoUsed) ? null : _submit),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -308,7 +289,7 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('已在浏览器打开支付宝收银台,完成支付后本窗口会自动确认到账。',
|
||||
child: Text('已在浏览器打开支付宝收银台,完成支付后本卡片会自动确认到账。',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
|
||||
),
|
||||
]),
|
||||
@@ -324,7 +305,8 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('订单号', style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
||||
Text('订单号',
|
||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
||||
const SizedBox(height: 4),
|
||||
Text(_order?.outTradeNo ?? '—',
|
||||
style: const TextStyle(
|
||||
@@ -338,8 +320,19 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text('关闭本窗口不影响到账:支付成功后授权自动续期,可稍后回本页刷新查看。',
|
||||
Text('离开本页不影响到账:支付成功后授权自动续期,可稍后回本页刷新查看。',
|
||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
DsButton('重新选择', onPressed: _backToPick),
|
||||
const SizedBox(width: 10),
|
||||
DsButton('我已完成支付',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => _poll(manual: true)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -364,8 +357,7 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
expires != null ? '门店授权已续期至 $expires。' : '门店授权已续期。',
|
||||
child: Text(expires != null ? '门店授权已续期至 $expires。' : '门店授权已续期。',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsTitle,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -375,10 +367,16 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
|
||||
const SizedBox(height: 10),
|
||||
Text('订单号 ${_order?.outTradeNo ?? '—'}',
|
||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: DsButton('完成', onPressed: _backToPick),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static String _fmt(int n) =>
|
||||
n.toString().replaceAllMapped(RegExp(r'(\d)(?=(\d{3})+$)'), (m) => '${m[1]},');
|
||||
static String _fmt(int n) => n
|
||||
.toString()
|
||||
.replaceAllMapped(RegExp(r'(\d)(?=(\d{3})+$)'), (m) => '${m[1]},');
|
||||
}
|
||||
@@ -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),
|
||||
// 兑换(原型 .redeem:input + 按钮,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),
|
||||
// 兑换(原型 .redeem:input + 按钮,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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |