Compare commits

...

9 Commits

Author SHA1 Message Date
wangjia 31024cfda6 chore: release client-v1.1.3
Deploy Client / build-client-web (push) Successful in 58s
Deploy Client / build-windows (push) Successful in 2m6s
Deploy Client / build-macos (push) Successful in 2m44s
Deploy Client / build-android (push) Successful in 1m25s
Deploy Client / build-ios (push) Successful in 3m11s
Deploy Client / release-deploy-client (push) Successful in 1m36s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-04 00:21:56 +08:00
wangjia 0e91768da4 feat(site): 下载页平台专属图标 + 检测卡放大重排
- 图标 sprite 新增 i-apple / i-android(simple-icons 品牌图标,填充式)与
  i-tablet-smartphone(lucide),先登记原型 icons.js 再同步官网 icons.njk
- 下载页 macOS/Android/iOS 卡片及当前平台检测卡换用专属图标(原为 copy/phone-call/funnel 错配)
- 检测卡高度加倍:icbox 44→84、标题 16→22、内边距加大,新增引导行与版本徽章,
  描述文字由 muted 灰改为正文色;窄屏按钮转全宽

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-04 00:08:01 +08:00
wangjia 24afb3b5fd refactor(client): 授权信息卡从「关于我们」迁至 设置→授权管理,替换授权到期时间卡
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 23:57:17 +08:00
wangjia cd6621d733 feat(client): 购买卡年付明示省 2 个月(tab 标注+划线原价+省钱说明行)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 23:50:20 +08:00
wangjia c3455ce7a7 feat(client): 授权管理拆四卡,首月特惠默认选中,关于页备案号加链接
- 授权管理面板拆四张卡:授权到期时间 / 在线购买续费 / 兑换券 / 到期降级规则;
  非管理员在购买卡位显示「联系管理员 + 查看套餐价格」引导
- 购买卡套餐 tab 首月特惠放第一位并默认选中;已享受过特惠则默认高级版年付
- 关于页页脚备案号可点击跳转工信部备案查询

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 23:46:40 +08:00
wangjia 2a5fea2647 fix(client): 关于页版本号不再显示 build 号,页脚版权加 ICP 备案号
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 23:39:18 +08:00
wangjia 02db118f61 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
2026-07-03 23:39:12 +08:00
wangjia 15ed05dc1e feat(client): 设置授权卡重排——在线购买/续费升为主 CTA
卡片改名「授权管理」,购买按钮升 primary 并排进到期统计条(窄屏通栏在统计条下方),
不再藏在「没有兑换券?」后面;兑换券降为次要入口;非管理员显示联系管理员+官网价格链接。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 23:21:34 +08:00
wangjia c15ffc7d32 chore: release site-v1.1.3(页脚 ICP 备案号)
Deploy Site / release-deploy-site (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 23:09:16 +08:00
21 changed files with 268 additions and 276 deletions
File diff suppressed because one or more lines are too long
+12
View File
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.3] - 2026-07-04
### 新功能
- 授权管理页改版为四张卡片:授权信息(状态/到期日/剩余天数,自「关于我们」迁入)、
在线购买/续费、兑换券、到期后降级规则,信息一目了然。
- 在线购买从弹窗改为设置页内嵌卡片,左侧菜单「授权兑换券」更名「授权管理」;
首月特惠默认选中,已享受过特惠的门店自动切换到高级版年付。
- 年付优惠明示:套餐价格旁标注划线原价与「年付立省 X 元,相当于免费用 2 个月」。
### 改进
- 关于页页脚新增 ICP 备案号并可点击跳转工信部查询;版本号不再显示 build 号。
## [1.1.2] - 2026-07-03
### 新功能
+5
View File
@@ -6,6 +6,11 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.3] - 2026-07-03
### 改进
- 页脚展示 ICP 备案号(京ICP备2026039814号),链接工信部备案查询。
## [1.1.2] - 2026-07-03
### 新功能
+17 -81
View File
@@ -6,9 +6,7 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/config/app_config.dart';
@@ -17,11 +15,9 @@ import '../../core/responsive/responsive.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/update/app_updater.dart';
import '../../core/utils/clock.dart';
import '../../core/utils/dialog_util.dart';
import '../../providers/changelog_provider.dart';
import '../../providers/feedback_provider.dart';
import '../../providers/license_provider.dart';
import '../../providers/update_provider.dart';
import '../../core/theme/app_fonts.dart';
import '../../widgets/brand_mark.dart';
@@ -56,14 +52,25 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
children: [
_heroCard(t),
_productCard(t),
_licenseCard(t),
_changelogCard(t),
// 页脚
Padding(
padding: const EdgeInsets.only(top: 4, bottom: 12),
child: Text('© 2026 岩美科技 · 保留所有权利',
textAlign: TextAlign.center,
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('© 2026 岩美科技 · 保留所有权利 · ',
style: TextStyle(
fontSize: AppDims.fsXs, color: t.faint)),
InkWell(
onTap: () =>
launchUrl(Uri.parse('https://beian.miit.gov.cn')),
child: Text('京ICP备2026039814号',
style: TextStyle(
fontSize: AppDims.fsXs, color: t.faint)),
),
],
),
),
],
),
@@ -281,13 +288,6 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: t.heading)),
const SizedBox(width: 6),
FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (_, snap) => Text(
snap.hasData ? 'build ${snap.data!.buildNumber}' : '',
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
),
const Spacer(),
DsSmallGhostButton(
label: '检查更新', icon: LucideIcons.refreshCw, onTap: _checkUpdate),
@@ -368,72 +368,8 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
}
}
// ── 卡3授权信息(3 cell)─────────────────────────────────
Widget _licenseCard(dynamic t) {
final lic = ref.watch(licenseProvider).valueOrNull;
final expires = lic?.expiresAt != null
? DateFormat('yyyy-MM-dd').format(lic!.expiresAt!)
: '';
// 状态/剩余天数走可注入时钟(golden 确定化)
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 mobile = context.isMobile;
final cells = [
_licCell(t, '授权状态', active ? '已授权' : '未授权',
color: active ? t.success : t.warn),
_licCell(t, '到期日', expires),
_licCell(t, '剩余天数', days != null ? '$days' : ''),
];
return _card(
t,
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_cardH(t, LucideIcons.shieldCheck, '授权信息'),
if (mobile)
Column(children: [
for (final c in cells)
Padding(padding: const EdgeInsets.only(bottom: 10), child: c),
])
else
Row(children: [
for (var i = 0; i < cells.length; i++) ...[
if (i > 0) const SizedBox(width: 14),
Expanded(child: cells[i]),
],
]),
]),
);
}
/// 原型 .lic-celllk 11 muted + lv 17/700/mono。
Widget _licCell(dynamic t, String label, String value, {Color? color}) =>
Container(
padding: 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,
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
const SizedBox(height: 6),
Text(value,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: color ?? t.heading)),
],
),
);
// ── 卡4:更新日志(timeline,默认 2 条 + 展开)───────────────
// ── 卡3更新日志(timeline,默认 2 条 + 展开)───────────────
// (原授权信息卡已迁至 设置 → 授权管理)
Widget _changelogCard(dynamic t) {
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? '';
final entries = ref.watch(changelogProvider).valueOrNull ?? const [];
@@ -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/:otnwebhook
// pay amount
import 'dart:async';
@@ -12,37 +12,27 @@ 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 _tabIdx = 0; // 0= 1= 2=
int _cycleIdx = 0; // 0= 1=
bool _submitting = false;
bool _checking = false;
@@ -52,9 +42,9 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
DateTime? _newExpiresAt;
Timer? _pollTimer;
bool get _isPromo => _tabIdx == 1;
bool get _isPromo => _tabIdx == 0;
LicensePlanGroup get _group =>
_tabIdx == 0 ? LicensePlans.standard : LicensePlans.pro;
_tabIdx == 1 ? LicensePlans.standard : LicensePlans.pro;
LicensePlan get _plan => _isPromo
? LicensePlans.promo
: (_cycleIdx == 0 ? _group.annual : _group.monthly);
@@ -64,7 +54,16 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
void initState() {
super.initState();
ref.read(licenseRepositoryProvider).promoUsed().then((used) {
if (mounted && used) setState(() => _promoUsed = true);
// ·
if (mounted && used) {
setState(() {
_promoUsed = true;
if (_phase == _Phase.pick && _isPromo) {
_tabIdx = 2;
_cycleIdx = 0;
}
});
}
});
}
@@ -77,8 +76,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 +88,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 +101,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 +124,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),
},
);
}
@@ -181,8 +158,8 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
Wrap(spacing: 10, runSpacing: 8, children: [
DsSeg(
items: [
'标准版',
_promoUsed ? '首月特惠(已享受)' : '🔥 首月特惠 ¥1',
'标准版',
'高级版',
],
index: _tabIdx,
@@ -190,7 +167,7 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
),
if (!_isPromo)
DsSeg(
items: const ['年付', '月付'],
items: const ['年付 · 省 2 个月', '月付'],
index: _cycleIdx,
onChanged: (i) => setState(() => _cycleIdx = i),
),
@@ -275,6 +252,17 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
fontFamilyFallback: AppFonts.monoFallback)),
const SizedBox(width: 8),
],
// 线×12 2
if (!_isPromo && _cycleIdx == 0) ...[
Text('¥${_fmt(_group.monthly.price * 12)}',
style: TextStyle(
fontSize: AppDims.fsSm,
color: t.faint,
decoration: TextDecoration.lineThrough,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
const SizedBox(width: 8),
],
Text('¥${_fmt(_plan.price)}',
style: TextStyle(
fontSize: 24,
@@ -288,9 +276,29 @@ class _PurchaseDialogState extends ConsumerState<_PurchaseDialog> {
fontFamilyFallback: AppFonts.monoFallback)),
],
),
if (!_isPromo && _cycleIdx == 0) ...[
const SizedBox(height: 6),
Text(
'月付 ¥${_fmt(_group.monthly.price)} × 12 个月 = ¥${_fmt(_group.monthly.price * 12)}'
'年付立省 ¥${_fmt(_group.monthly.price * 12 - _group.annual.price)},相当于免费用 2 个月',
style: TextStyle(fontSize: AppDims.fsXs, color: t.success)),
],
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 +316,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 +332,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 +347,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 +384,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 +394,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]},');
}
+104 -98
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();
@@ -691,45 +691,72 @@ class _LicensePanelState extends ConsumerState<_LicensePanel> {
final expires = lic?.expiresAt != null
? DateFormat('yyyy-MM-dd').format(lic!.expiresAt!)
: '';
// 剩余天数走可注入时钟(golden 确定化),不用 model 内的 DateTime.now()
// 状态/剩余天数走可注入时钟(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 = [
_licCell(t, '授权状态', active ? '已授权' : '未授权',
color: active ? t.success : t.warn),
_licCell(t, '到期日', expires),
_licCell(t, '剩余天数', days != null ? '$days' : ''),
];
return _SettingsCard(
title: '授权兑换券',
desc: '本系统以「时长兑换券」授权,输入兑换券短码即可续期,无需订阅',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 原型 .lic 统计条:bg 底 / border / r-md / pad 18 20
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),
),
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── 卡1:授权信息(3 cell,自「关于我们」迁入)──
_SettingsCard(
title: '授权信息',
desc: '当前门店的授权状态与有效期,续期时长在现有到期时间上叠加',
child: context.isMobile
? Column(children: [
for (final c in licCells)
Padding(
padding: const EdgeInsets.only(bottom: 10), child: c),
])
: Row(children: [
for (var i = 0; i < licCells.length; i++) ...[
if (i > 0) const SizedBox(width: 14),
Expanded(child: licCells[i]),
],
]),
),
const SizedBox(height: 16),
// ── 卡2:在线购买 / 续费(后端 purchase 接口 admin only
// 非管理员引导看官网价格页)──
if (isAdmin)
const _SettingsCard(
title: '在线购买 / 续费',
desc: '选择套餐支付宝支付,到账后授权自动续期',
child: PurchaseCard(),
)
else
_SettingsCard(
title: '在线购买 / 续费',
desc: '在线购买仅门店管理员可操作',
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),
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),
// 兑换(原型 .redeeminput + 按钮,max 520
ConstrainedBox(
const SizedBox(height: 16),
// ── 卡3兑换(原型 .redeeminput + 按钮,max 520──
_SettingsCard(
title: '兑换券',
desc: '输入兑换券短码续期',
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
@@ -758,73 +785,52 @@ class _LicensePanelState extends ConsumerState<_LicensePanel> {
],
),
),
const SizedBox(height: 14),
Row(children: [
Text('没有兑换券?',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
// 在线购买仅管理员(后端 purchase 接口 admin only);其他角色引导看官网价格页
if (isAdmin)
DsButton('在线购买 / 续费',
variant: DsBtnVariant.accent,
onPressed: () => showPurchaseDialog(context))
else
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)),
),
],
),
),
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)),
),
],
),
],
),
),
],
);
}
Widget _stat(dynamic t, String label, String value, {bool warn = false}) =>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
const SizedBox(height: 4),
Text(value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: warn ? t.warn : t.heading)),
],
/// 原型 .lic-cell(与「关于我们」同款):lk 11 muted + lv 17/700/mono。
Widget _licCell(dynamic t, String label, String value, {Color? color}) =>
Container(
padding: 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,
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
const SizedBox(height: 6),
Text(value,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: color ?? t.heading)),
],
),
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 KiB

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 89 KiB

+1 -1
View File
@@ -2,7 +2,7 @@
"name": "岩美酒库管理系统",
"tagline": "为酒行与酒店设计的库存管理平台",
"copyright": "© 2026 岩美科技 · 保留所有权利",
"icp": "",
"icp": "京ICP备2026039814号",
"support": {
"email": "yammy2023@163.com",
"wechat": ""
File diff suppressed because one or more lines are too long
+24 -16
View File
@@ -3,15 +3,19 @@ layout: base.njk
title: 下载
description: 下载岩美酒库管理系统客户端:Windows、macOS、Android、iOS 与免安装 Web 版。
pageStyle: |
.dl-hero{background:var(--mk-hero-grad); color:var(--on-accent); padding:64px 0 72px; text-align:center;}
.dl-hero{background:var(--mk-hero-grad); color:var(--on-accent); padding:64px 0 88px; text-align:center;}
.dl-hero h1{color:var(--on-accent); font-size:var(--mk-h2); font-weight:800;}
.dl-hero p{color:var(--on-accent-faint); margin:12px auto 0; max-width:36em;}
.detect{max-width:640px; margin:-40px auto 0; position:relative; z-index:2;}
.detect .card{display:flex; align-items:center; gap:18px; box-shadow:var(--sh-2); padding:20px 22px;}
.detect .icbox{width:44px; height:44px; border-radius:var(--r-lg); flex:none; display:flex; align-items:center; justify-content:center;}
.detect .icbox .ic{width:22px; height:22px;}
.detect b{display:block; color:var(--heading); font-size:16px;}
.detect span{color:var(--muted); font-size:var(--fs-sm);}
.detect{max-width:720px; margin:-56px auto 0; position:relative; z-index:2;}
.detect .card{display:flex; align-items:center; gap:26px; box-shadow:var(--sh-2); padding:34px 36px;}
.detect .icbox{width:84px; height:84px; border-radius:var(--r-lg); flex:none; display:flex; align-items:center; justify-content:center;}
.detect .icbox .ic{width:42px; height:42px;}
.detect .detect-info{flex:1; min-width:0;}
.detect .detect-eyebrow{color:var(--primary); font-size:var(--fs-xs); font-weight:800; letter-spacing:1px; margin-bottom:5px;}
.detect .detect-title{display:flex; align-items:center; gap:10px; margin-bottom:7px; flex-wrap:wrap;}
.detect b{color:var(--heading); font-size:22px; font-weight:800; line-height:1.3;}
.detect .detect-ver{font-family:var(--font-mono); font-size:var(--fs-xs); font-weight:700; color:var(--primary); background:var(--info-soft); padding:3px 10px; border-radius:var(--r-pill);}
.detect .detect-desc{display:block; color:var(--text); font-size:15px; line-height:1.7;}
.detect .btn{margin-left:auto; flex:none;}
.dl-grid{display:grid; grid-template-columns:repeat(5,1fr); gap:14px; margin-top:46px;}
.dl-card{background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); padding:20px 18px; display:flex; flex-direction:column; text-align:center;}
@@ -35,7 +39,7 @@ pageStyle: |
.changelog-body ul{margin:0; padding-left:20px; color:var(--muted); font-size:14px; line-height:1.8;}
.changelog-intro{color:var(--muted); font-size:14px; margin:0 0 4px;}
@media (max-width:900px){ .dl-grid{grid-template-columns:repeat(2,1fr);} }
@media (max-width:600px){ .dl-grid{grid-template-columns:1fr;} .detect .card{flex-wrap:wrap;} .changelog-entry{grid-template-columns:1fr;} }
@media (max-width:600px){ .dl-grid{grid-template-columns:1fr;} .detect .card{flex-wrap:wrap; padding:26px 22px; gap:18px;} .detect .btn{margin-left:0; width:100%;} .changelog-entry{grid-template-columns:1fr;} }
---
<header class="dl-hero">
@@ -48,11 +52,15 @@ pageStyle: |
<div class="detect container">
<div class="card">
<div class="icbox ic-info" id="detect-icon"><svg class="ic"><use href="#i-ic01"/></svg></div>
<div>
<b id="detect-name">Web 版</b>
<span id="detect-desc">已自动识别你的设备,推荐以下版本。</span>
<div class="detect-info">
<div class="detect-eyebrow">已识别你的设备 · 为你推荐</div>
<div class="detect-title">
<b id="detect-name">Web 版</b>
<span class="detect-ver version-badge">v{{ changelog[0].version }}</span>
</div>
<span class="detect-desc" id="detect-desc">浏览器直接访问,功能完整,无需下载安装。</span>
</div>
<a class="btn primary" id="detect-btn" href="{{ site.appBaseUrl }}/app/"><span>立即使用</span></a>
<a class="btn primary lg" id="detect-btn" href="{{ site.appBaseUrl }}/app/"><span>立即使用</span></a>
</div>
</div>
@@ -67,7 +75,7 @@ pageStyle: |
<a class="btn primary" id="windows-dl-btn" href="{{ site.appBaseUrl }}/downloads/jiu-windows-x64-setup.exe"><svg class="ic" style="width:15px;height:15px"><use href="#i-download"/></svg>下载</a>
</div>
<div class="dl-card" data-platform="macos">
<div class="icbox ic-info"><svg class="ic"><use href="#i-ic25"/></svg></div>
<div class="icbox ic-info"><svg class="ic"><use href="#i-apple"/></svg></div>
<h3>macOS</h3>
<div class="req">Apple Silicon / Intel</div>
<div class="dl-status">✓ 已签名公证</div>
@@ -81,14 +89,14 @@ pageStyle: |
<a class="btn primary" href="{{ site.appBaseUrl }}/app/">立即使用</a>
</div>
<div class="dl-card" data-platform="android">
<div class="icbox ic-warn"><svg class="ic"><use href="#i-ic43"/></svg></div>
<div class="icbox ic-warn"><svg class="ic"><use href="#i-android"/></svg></div>
<h3>Android</h3>
<div class="req">Android 6.0 及以上</div>
<div class="dl-status">✓ APK 直装</div>
<a class="btn primary" id="android-dl-btn" href="{{ site.appBaseUrl }}/downloads/jiu-android.apk"><svg class="ic" style="width:15px;height:15px"><use href="#i-download"/></svg>下载</a>
</div>
<div class="dl-card" data-platform="ios" id="ios-card">
<div class="icbox ic-wine"><svg class="ic"><use href="#i-ic22"/></svg></div>
<div class="icbox ic-wine"><svg class="ic"><use href="#i-tablet-smartphone"/></svg></div>
<h3>iOS</h3>
<div class="req">iPhone / iPad</div>
<div class="dl-status soon" id="ios-status">TestFlight 内测中</div>
@@ -137,7 +145,7 @@ pageStyle: |
<script>
(function() {
var APP_BASE = '{{ site.appBaseUrl }}';
var SPRITE = { web:'#i-ic01', ios:'#i-ic22', android:'#i-ic43', windows:'#i-ic63', macos:'#i-ic25' };
var SPRITE = { web:'#i-ic01', ios:'#i-tablet-smartphone', android:'#i-android', windows:'#i-ic63', macos:'#i-apple' };
var platforms = {
web: { name: 'Web 版', desc: '浏览器直接访问,功能完整,无需下载安装。', btnText: '立即使用 Web 版', btnHref: APP_BASE + '/app/', available: true },
ios: { name: 'iOS', desc: 'iPhone 与 iPad 通用,通过 TestFlight 安装。', btnText: '敬请期待', available: false },