Files
jiu/client/lib/screens/settings/license_panel.dart
T

217 lines
8.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// screens/settings/license_panel.dart — 授权管理「授权信息」tab 内容
// 2026-07-10 独立一级屏三 tab 化:本组件收窄为原四卡去掉「在线购买续费」,
// 该卡已提升为 LicenseScreen 的独立 tab2,见 screens/license/license_screen.dart)。
// 三卡:授权信息三格 / 兑换券 / 到期降级规则 + iOS 合规态「联系客服」卡。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/config/license_copy.dart';
import '../../core/config/store_compliance.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/app_fonts.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/utils/clock.dart';
import '../../providers/license_provider.dart';
import '../../widgets/ds/ds_atoms.dart';
import '../../widgets/ds/ds_toast.dart';
import '../../widgets/write_guard.dart';
import 'settings_card.dart';
class LicensePanel extends ConsumerStatefulWidget {
const LicensePanel({super.key});
@override
ConsumerState<LicensePanel> createState() => _LicensePanelState();
}
class _LicensePanelState extends ConsumerState<LicensePanel> {
final _voucherCtrl = TextEditingController();
bool _redeeming = false;
@override
void dispose() {
_voucherCtrl.dispose();
super.dispose();
}
Future<void> _redeem() async {
final code = _voucherCtrl.text.trim();
if (code.isEmpty) {
showDsToast(context, '请输入兑换券短码');
return;
}
setState(() => _redeeming = true);
try {
await ref.read(licenseRepositoryProvider).activate(code);
ref.invalidate(licenseProvider);
_voucherCtrl.clear();
if (mounted) {
showDsToast(context, '兑换成功,已续期 ✓', bg: context.tokens.success);
}
} catch (e) {
if (mounted) {
showDsToast(context, '兑换失败:$e', bg: context.tokens.danger);
}
} finally {
if (mounted) setState(() => _redeeming = false);
}
}
@override
Widget build(BuildContext context) {
final t = context.tokens;
final lic = ref.watch(licenseProvider).valueOrNull;
final expires = lic?.expiresAt != null
? DateFormat('yyyy-MM-dd').format(lic!.expiresAt!)
: '';
// 状态/剩余天数走可注入时钟(golden 确定化),不用 model 内的 DateTime.now()
final active = lic?.isActive == true &&
(lic?.expiresAt == null || lic!.expiresAt!.isAfter(appNow()));
final days = lic?.expiresAt != null
? lic!.expiresAt!.difference(appNow()).inDays.clamp(0, 99999)
: null;
final licCells = [
// 值为中文词(已授权/未授权):不走等宽字体(JetBrains Mono 无 CJK 字形,
// 回退链缺 NotoSansSC 会在 golden 环境 tofu),其余两格是纯数字/日期保留 mono。
_licCell(t, '授权状态', active ? '已授权' : '未授权',
color: active ? t.success : t.warn, mono: false),
_licCell(t, '到期日', expires),
_licCell(t, '剩余天数', days != null ? '$days' : '', mono: false),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── 卡1:授权信息(3 cell 横排,窄屏同样横排、间距收窄)──
SettingsCard(
title: '授权信息',
desc: '当前门店的授权状态与有效期,续期时长在现有到期时间上叠加',
child: Row(children: [
for (var i = 0; i < licCells.length; i++) ...[
if (i > 0) SizedBox(width: context.isMobile ? 8 : 14),
Expanded(child: licCells[i]),
],
]),
),
const SizedBox(height: 16),
// ── 卡2:兑换券(原型 .redeeminput + 按钮,max 520)──
SettingsCard(
title: '兑换券',
desc: '输入兑换券短码续期',
child: 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(height: 16),
// ── 卡4:到期后的降级规则(现有能力保留)──
SettingsCard(
title: '到期后的降级规则',
desc: '授权到期后系统各功能的可用性说明',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final note in LicenseCopy.degradationNotes())
Padding(
padding: const EdgeInsets.only(top: 3),
child: Text('· $note',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
),
],
),
),
// ── 卡5:联系客服(仅 iOS 形态,补位购买卡;措辞保持中性,
// 不得出现购买/续费引导字样——苹果 3.1.1 红线)──
if (hideExternalPurchaseUi) ...[
const SizedBox(height: 16),
SettingsCard(
title: '联系客服',
desc: '授权与使用问题可通过邮件联系我们',
child: Row(children: [
Text('客服邮箱 · ',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
InkWell(
onTap: () =>
launchUrl(Uri(scheme: 'mailto', path: supportEmail)),
child: Text(supportEmail,
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
fontFamily: AppFonts.mono,
color: t.primary)),
),
]),
),
],
],
);
}
/// 原型 .lic-cell(与「关于我们」同款):lk 11 muted + lv 17/700/mono。
/// [mono] 为 false 时值不走等宽字体(CJK 词如「已授权」,mono 回退链无
/// CJK 字形会 tofu)。
Widget _licCell(dynamic t, String label, String value,
{Color? color, bool mono = true}) =>
Container(
// 窄屏三格横排时收窄内距,值文本按格宽缩放防溢出(如 2026-06-25
padding: context.isMobile
? const EdgeInsets.fromLTRB(10, 12, 10, 12)
: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.borderSubtle),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
const SizedBox(height: 6),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(value,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
fontFamily: mono ? AppFonts.mono : null,
fontFamilyFallback: mono ? AppFonts.monoFallback : null,
color: color ?? t.heading)),
),
],
),
);
}