Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b37568fdb | |||
| e0fdb8c12f | |||
| e022494f63 | |||
| 31024cfda6 | |||
| 0e91768da4 | |||
| 24afb3b5fd | |||
| cd6621d733 | |||
| c3455ce7a7 | |||
| 2a5fea2647 | |||
| 02db118f61 | |||
| 15ed05dc1e |
@@ -0,0 +1,39 @@
|
||||
name: Design Source Checks
|
||||
|
||||
# 设计真相源检查(治理规则见 CLAUDE.md「设计真相源分层」):
|
||||
# PR 与合并 main 时自动跑四道闸,全部纯静态检查(node + dart format),
|
||||
# 不跑 golden/fidelity(重型像素闸维持「定期体检」定位,本地手动跑)。
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: checks-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
design-source:
|
||||
runs-on: mac
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: 原型设计系统闸(check-ds 12 道)
|
||||
run: node .superpowers/prototype/tools/check-ds.mjs
|
||||
|
||||
- name: L1 真相源同源闸(tokens 快照 / icons / 官网 token 值 / web hex)
|
||||
run: node tools/check-l1-sync.mjs
|
||||
|
||||
- name: Flutter 颜色单源闸(check_ds_code)
|
||||
run: node client/tool/check_ds_code.mjs
|
||||
|
||||
- name: token codegen 新鲜度(regen → format → 与入库产物零 diff)
|
||||
run: |
|
||||
cd client
|
||||
node tool/gen_tokens.mjs
|
||||
dart format lib/core/theme/app_tokens.g.dart lib/core/theme/app_dims.g.dart lib/core/theme/app_chrome.g.dart
|
||||
git diff --exit-code -- lib/core/theme/app_tokens.g.dart lib/core/theme/app_dims.g.dart lib/core/theme/app_chrome.g.dart
|
||||
@@ -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
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
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.4] - 2026-07-04
|
||||
|
||||
### 改进
|
||||
- 下载页 macOS / Android / iOS 换用平台专属图标(Apple 标、Android 机器人、手机平板)。
|
||||
- 「当前设备推荐」卡片放大一倍:新增识别引导行与实时版本徽章,描述文字改用正文色更醒目,
|
||||
手机上按钮转为全宽。
|
||||
|
||||
## [1.1.3] - 2026-07-03
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -254,6 +254,19 @@ cd client && flutter test
|
||||
- 使用 `dart:io` 的 `Platform` 类前,必须先检查 `kIsWeb`(`import 'package:flutter/foundation.dart'`)
|
||||
- Web 平台不支持 `dart:io`,直接使用会在 Web 构建时崩溃
|
||||
|
||||
### 设计真相源分层(原型漂移治理,2026-07-04 拍板)
|
||||
|
||||
原型(`.superpowers/prototype/`)与真实页面**不追求全量永久同步**,按层分治:
|
||||
|
||||
- **L1 设计系统层**(`tokens.css` / `atoms.css` / `mobile-atoms.css` / `icons.js` + `index.html` 登记):原型是**单一真源**。新增颜色/组件/图标必须**先登记原型再同步**代码与官网(如 `web/_includes/icons.njk`)。闸:pre-commit `check-ds.mjs` 12 道(本地钩子,新机器先跑 `sh scripts/hooks/install.sh`)+ 代码侧 `client/tool/check_ds_code.mjs`。
|
||||
- **L2 屏级三态**(台账 = `design/CONTRACT.md` 块 4「真相源」列):
|
||||
- `同步`:注册于 `tools/screens.mjs`,fidelity 像素闸生效。整屏改版须原型先行;小迭代改后跑 fidelity 确认阈内,超阈则同步原型或降级快照。
|
||||
- `快照`:原型退役为历史参考,**golden + CONTRACT 文字规格为准**;小迭代只更 golden,**不回填原型**。
|
||||
- `代码先行`:无原型屏,golden 为唯一像素基准。
|
||||
- **L3 新屏/整屏改版**:design-first 不变(原型 → CONTRACT → 实现 → fidelity 验收),建成后入 `同步` 态。
|
||||
- **漂移体检**:不逐提交强制同步;大版本/整屏改版前跑 `node tools/fidelity.mjs`,按报告逐屏决定「重新对齐原型」或「降级快照」,结果回填 CONTRACT。
|
||||
- **CI 闸**(`.gitea/workflows/checks.yml`,PR + main push 自动跑):① `check-ds.mjs` 原型 12 道;② `tools/check-l1-sync.mjs` L1 同源四道(tokens 快照逐字节 / icons 两端同集同内容 / 官网 token 值对齐原型主题 A / web 硬编码 hex,白名单 #fff、#1677ff,`ds-allow` 可豁免);③ `client/tool/check_ds_code.mjs` Flutter 颜色单源;④ codegen 新鲜度(regen→`dart format`→与入库产物零 diff)。本地可直接跑同名命令。
|
||||
|
||||
### 表格与列头筛选(ds 真相源)
|
||||
- 列表屏统一 `DsTable`(`widgets/ds/ds_table.dart`:toolbar+表格+pager 连成一卡),列定义 `DsColumn`,可筛选列走其列头漏斗(`filtered` 态高亮),可隐藏列走列设置菜单
|
||||
- 不要在 toolbar 放独立筛选按钮,筛选入口内嵌列头或工具栏 `DsChip`
|
||||
|
||||
@@ -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-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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// ── 卡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/:otn,webhook 续期到账后自动刷新授权并展示新到期日。
|
||||
// 价格仅展示,实付以 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,50 +124,27 @@ 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),
|
||||
// 与授权卡兑换区一致收窄内容宽度,避免宽屏卡片内元素拉伸过长
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
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()),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]},');
|
||||
}
|
||||
@@ -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,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
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),
|
||||
// ── 卡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),
|
||||
// 兑换(原型 .redeem:input + 按钮,max 520)
|
||||
ConstrainedBox(
|
||||
// ── 卡3:兑换券(原型 .redeem:input + 按钮,max 520)──
|
||||
_SettingsCard(
|
||||
title: '兑换券',
|
||||
desc: '输入兑换券短码续期',
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@@ -758,30 +785,32 @@ 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),
|
||||
// 过期降级说明(现有能力保留)
|
||||
// ── 卡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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 原型 .lic-cell(与「关于我们」同款):lk 11 muted + lv 17/700/mono。
|
||||
Widget _licCell(dynamic t, String label, String value, {Color? color}) =>
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: t.bg,
|
||||
border: Border.all(color: t.borderSubtle),
|
||||
@@ -790,41 +819,18 @@ class _LicensePanelState extends ConsumerState<_LicensePanel> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('到期后的降级规则',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: t.heading)),
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
fontSize: AppDims.fsH2,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
color: warn ? t.warn : t.heading)),
|
||||
color: color ?? t.heading)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 379 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 408 KiB |
|
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 |
@@ -1,8 +1,11 @@
|
||||
# 设计契约 — 岩美酒库 Flutter 还原(Phase 1 起:库存列表)
|
||||
|
||||
> design-distill 阶段 1 产物。像素以 `.superpowers/prototype/` 原型为**唯一基准**;
|
||||
> token 走 codegen 单源(已在 P0 落地);还原由截图 diff 验收,不靠目测。
|
||||
> design-distill 阶段 1 产物。token 走 codegen 单源(已在 P0 落地);还原由截图 diff 验收,不靠目测。
|
||||
> 本文件是**执行真相源**(与 plan `.md` 同性质,机器/diff 引用),随逐屏推进增补。
|
||||
>
|
||||
> **真相源分层(2026-07-04 拍板,全文见 CLAUDE.md「设计真相源分层」)**:原型仅对
|
||||
> 设计系统层(tokens/atoms/icons)与「真相源=同步」态屏是唯一基准;`快照`/`代码先行`
|
||||
> 态屏以 **golden + 本文件文字规格**为准,小迭代不回填原型。屏的状态登记在块 4「真相源」列。
|
||||
|
||||
## 阶段 0 · 归一化产物(已就位)
|
||||
|
||||
@@ -87,23 +90,38 @@ AppTokens 目前**仅颜色**。原型还驱动:
|
||||
|
||||
## 块 4 · 逐屏验收清单(每屏 × 主题;diff 闸)
|
||||
|
||||
| 屏 | 原型 | Flutter | a(基准) | b | c |
|
||||
|---|---|---|---|---|---|
|
||||
| 库存列表(桌面) | `inventory.html` | `inventory_list_screen.dart` | ✅ KPI/徽章/列/副标/toolbar 已还原 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 库存列表(移动) | `m-inventory.html` | 同上窄屏(卡片流) | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 往来单位(桌面) | `partners.html` | `partners_screen.dart` | ✅ **Phase2 重建**:单表+类型 chips/应收应付列(finance summary)/详情抽屉/frow 弹窗;fidelity 2.5–3.5%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 往来单位(移动) | `m-partners.html` | 同上窄屏(卡片流) | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 基础数据(桌面) | `products.html` | `products_screen.dart` | ✅ **Phase2 重建**:.seg 5 tab(档案/介绍/系列/规格/仓库)+三套视图+编辑抽屉复用;fidelity 2.4–3.5%≤8%(已知差异见下) | ✅ fidelity | ✅ fidelity |
|
||||
| 基础数据(移动) | `m-products.html` | 同上窄屏(卡片流) | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 入库列表(桌/移) | `stock-in-list.html` | `stock_in_list_screen.dart` | ✅ 副标 + 共享 StatusPill(草稿/待审/已审/已拒) | ✅ golden | ✅ golden |
|
||||
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | ✅ 副标 + 共享 StatusPill | ✅ golden | ✅ golden |
|
||||
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);fidelity 3.0–4.2%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | ✅ **Phase2 重建**:subnav 5 面板(门店/用户预览/编号规则/授权兑换券/偏好);假「系统参数」已删;fidelity 1.7–2.3%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 用户管理(桌/移) | `users.html` | `users_screen.dart`(`/settings/users`) | ✅ **Phase2 新独立页**:KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.3–2.1%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 关于我们(桌/移) | `about.html` | `about_screen.dart` | ✅ **Phase2 重建**:Hero/产品信息/授权信息/更新日志 timeline(/public/release 同源);fidelity 2.2–2.7%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 登录 | `login.html` | `login_screen.dart` | ✅ **Phase2 重建(2026-07-03)**:两栏卡片(品牌渐变面板+表单)+右上主题切换器+记住我/忘记密码+lg 主按钮;fidelity 1.4–2.1%≤8%(已知差异见下) | ✅ fidelity | ✅ fidelity |
|
||||
| 注册新门店 | `register.html` | `register_screen.dart` | ✅ **Phase2 重建(2026-07-03)**:品牌面板三步时间线+grid2 表单+协议勾选;**暂不入 fidelity 闸**(少 门店编号/兑换券 两字段致结构错位,用户拍板先不加,见下) | ✅ golden ×3 | ✅ golden ×3 |
|
||||
**「真相源」列三态**(治理规则见 CLAUDE.md「设计真相源分层」):
|
||||
`同步` = 注册于 `tools/screens.mjs`,桌面走 fidelity 像素闸(移动走 golden),整屏改版须原型先行;
|
||||
`快照` = 原型退役为历史参考,golden + 本表文字规格为准,小迭代不回填原型;
|
||||
`代码先行` = 无原型屏,golden 为唯一像素基准(清单见块 4 末尾小节)。
|
||||
|
||||
| 屏 | 原型 | Flutter | 真相源 | a(基准) | b | c |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 库存列表(桌面) | `inventory.html` | `inventory_list_screen.dart` | 同步 | ✅ KPI/徽章/列/副标/toolbar 已还原 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 库存列表(移动) | `m-inventory.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 往来单位(桌面) | `partners.html` | `partners_screen.dart` | 同步 | ✅ **Phase2 重建**:单表+类型 chips/应收应付列(finance summary)/详情抽屉/frow 弹窗;fidelity 2.5–3.5%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 往来单位(移动) | `m-partners.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 基础数据(桌面) | `products.html` | `products_screen.dart` | 同步 | ✅ **Phase2 重建**:.seg 5 tab(档案/介绍/系列/规格/仓库)+三套视图+编辑抽屉复用;fidelity 2.4–3.5%≤8%(已知差异见下) | ✅ fidelity | ✅ fidelity |
|
||||
| 基础数据(移动) | `m-products.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||
| 入库列表(桌/移) | `stock-in-list.html` | `stock_in_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill(草稿/待审/已审/已拒) | ✅ golden | ✅ golden |
|
||||
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill | ✅ golden | ✅ golden |
|
||||
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);fidelity 3.0–4.2%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**:subnav 5 面板(门店/用户预览/编号规则/授权管理/偏好);假「系统参数」已删;fidelity 1.7–2.3%≤8%;**授权管理面板为代码先行区**(见下小节) | ✅ fidelity | ✅ fidelity |
|
||||
| 用户管理(桌/移) | `users.html` | `users_screen.dart`(`/settings/users`) | 同步 | ✅ **Phase2 新独立页**:KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.3–2.1%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 关于我们(桌/移) | `about.html` | `about_screen.dart` | 同步 | ✅ **Phase2 重建**:Hero/产品信息/更新日志 timeline(/public/release 同源);fidelity 2.2–2.7%≤8%;授权信息卡 2026-07-04 迁至 设置→授权管理 | ✅ fidelity | ✅ fidelity |
|
||||
| 登录 | `login.html` | `login_screen.dart` | 同步 | ✅ **Phase2 重建(2026-07-03)**:两栏卡片(品牌渐变面板+表单)+右上主题切换器+记住我/忘记密码+lg 主按钮;fidelity 1.4–2.1%≤8%(已知差异见下) | ✅ fidelity | ✅ fidelity |
|
||||
| 注册新门店 | `register.html` | `register_screen.dart` | 快照 | ✅ **Phase2 重建(2026-07-03)**:品牌面板三步时间线+grid2 表单+协议勾选;**不入 fidelity 闸**(少 门店编号/兑换券 两字段致结构错位,用户拍板先不加,见下) | ✅ golden ×3 | ✅ golden ×3 |
|
||||
|
||||
### 代码先行屏/面板(无原型,golden 为唯一像素基准)
|
||||
|
||||
2026-07-04 用户拍板:以下功能为代码先行交付,**不反向补建原型**;后续小迭代只更 golden 与本表规格。
|
||||
|
||||
| 屏/面板 | Flutter | 像素基准 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 设置→授权管理面板(授权信息/在线购买续费/兑换券/降级规则 四卡) | `settings_screen.dart` `_LicensePanel` | settings golden ×3(仅锁面板 0 门店信息;子导航文字变更会入镜) | 2026-07 授权/购买迭代产物;原型 `settings.html` 仍是旧「授权兑换券」面板,视为历史快照 |
|
||||
| 购买卡(首月特惠/标准/高级 tab + 年付月付 + 支付轮询三态) | `purchase_card.dart` | 无独立 golden;改动后人工目检 + `flutter test` 回归 | 价格展示以 `license_plans.dart` 为源,实付以 pay 侧为准 |
|
||||
| 官网 定价区/checkout/license-result/profile/logout | `web/index.njk` `checkout.njk` 等 | 人工验收(web 无 golden 体系) | 原型 `pricing.html`/`checkout.html` 为当初设计参考,已漂移,视为历史快照 |
|
||||
|
||||
> **共享组件杠杆**:`StatusBadge`(入/出库单据状态)一处升级两屏受益;`StatusPill`/`KpiCard` 现已被库存/往来/入出库/财务/设备/设置/关于复用——新屏直接套,无需重做徽章/卡片。
|
||||
|
||||
@@ -118,7 +136,7 @@ AppTokens 目前**仅颜色**。原型还驱动:
|
||||
| Web 营销站 | ✅ **已对齐品牌**:`web/assets/color.css` 的 `--brand-500:#2563AC` / `--accent` 酒红 / 灰阶 与原型令牌一致,roadmap「Web 镜像品牌」已满足 |
|
||||
|
||||
### 仍属「新建功能」(Phase 6,非重建既有屏)
|
||||
应用内注册 / 购买·套餐·下单(设计+前端壳,**支付不接**)——这是 net-new 功能开发,需各自 spec→设计→实现,不在「既有屏设计语言升级」范围内。
|
||||
~~应用内注册 / 购买·套餐·下单~~ → **2026-07 已交付**(支付宝支付打通,App 内购买卡 + 官网 checkout),按「代码先行」登记于块 4 末尾小节,不再走 design-first 补票。后续 net-new 功能仍各自 spec→设计→实现。
|
||||
|
||||
> **Phase 2 已解决(2026-07-02)**:
|
||||
> - **往来单位**:应收/应付列改由 `GET /finance/summary`(按 partner 聚合未结清)供给,两 Tab 改回原型的类型 chips 单表;详情抽屉近期单据走 `finance/records?partner_id=`。
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
// tools/check-l1-sync.mjs — L1 设计真相源同源闸(四道,纯 Node 零依赖)
|
||||
//
|
||||
// 治理依据:CLAUDE.md「设计真相源分层」L1 层——tokens/atoms/icons 以
|
||||
// .superpowers/prototype/ 为单一真源,client 与 web 只能是它的同步副本。
|
||||
// CI(.gitea/workflows/checks.yml,PR + main push)与本地均可直接跑:
|
||||
// node tools/check-l1-sync.mjs
|
||||
//
|
||||
// 四道检查(任一违规 exit 1):
|
||||
// ① tokens 快照同源 原型 tokens.css ≡ client token_source/tokens.css(逐字节)
|
||||
// ② icons 同源 原型 icons.js 与 web icons.njk 的 symbol 同集且同内容
|
||||
// ③ 官网 token 值同源 web/assets/tokens.css 与原型(:root+主题A) 同名 token 值一致
|
||||
// (官网独有 --mk-*/兼容别名层、App 外壳专属 token 不算违规)
|
||||
// ④ web 硬编码色扫描 web/**/*.{njk,css,js} 禁裸 hex;白名单 #fff/#ffffff/#1677ff
|
||||
// (对齐原型 check-ds 豁免口径);行内 `ds-allow` 注释可豁免;
|
||||
// 排除 web/assets/tokens.css(token 定义位)与 web/dist/。
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const read = (p) => readFileSync(join(ROOT, p), 'utf8');
|
||||
const problems = [];
|
||||
|
||||
// ── ① tokens 快照同源 ──────────────────────────────────────
|
||||
{
|
||||
const proto = read('.superpowers/prototype/tokens.css');
|
||||
const snap = read('client/lib/core/theme/token_source/tokens.css');
|
||||
if (proto !== snap) {
|
||||
problems.push(
|
||||
'[tokens快照] .superpowers/prototype/tokens.css 与 ' +
|
||||
'client/lib/core/theme/token_source/tokens.css 不一致 —— ' +
|
||||
'改原型后需拷贝快照并重跑 client/lib/core/theme/tool/gen_tokens.mjs',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ② icons 同源 ───────────────────────────────────────────
|
||||
{
|
||||
const symbols = (s) => {
|
||||
const map = {};
|
||||
for (const [, id, body] of s.matchAll(/<symbol id="([^"]+)"[^>]*>(.*?)<\/symbol>/gs)) {
|
||||
map[id] = body.trim();
|
||||
}
|
||||
return map;
|
||||
};
|
||||
const a = symbols(read('.superpowers/prototype/screens/icons.js'));
|
||||
const b = symbols(read('web/_includes/icons.njk'));
|
||||
for (const id of Object.keys(a)) {
|
||||
if (!(id in b)) problems.push(`[icons] symbol "${id}" 仅原型有,web/_includes/icons.njk 缺 —— 改原型后同步官网`);
|
||||
}
|
||||
for (const id of Object.keys(b)) {
|
||||
if (!(id in a)) problems.push(`[icons] symbol "${id}" 仅官网有 —— 图标必须先登记原型 icons.js 再同步`);
|
||||
}
|
||||
for (const id of Object.keys(a)) {
|
||||
if (id in b && a[id] !== b[id]) problems.push(`[icons] symbol "${id}" 两端内容不一致`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ③ 官网 token 值同源 ────────────────────────────────────
|
||||
{
|
||||
const vars = (css) =>
|
||||
Object.fromEntries(
|
||||
[...css.matchAll(/(--[\w-]+)\s*:\s*([^;}]+)[;}]/g)].map(([, k, v]) => [k, v.replace(/\s+/g, '')]),
|
||||
);
|
||||
const proto = read('.superpowers/prototype/tokens.css');
|
||||
const root = proto.match(/:root\s*{([^}]*)}/s)?.[1] ?? '';
|
||||
const themeA = proto.match(/\[data-theme="a"\]\s*{([^}]*)}/s)?.[1] ?? '';
|
||||
const pv = { ...vars(root), ...vars(themeA) };
|
||||
const wv = vars(read('web/assets/tokens.css'));
|
||||
for (const [k, v] of Object.entries(pv)) {
|
||||
if (k in wv && wv[k] !== v) {
|
||||
problems.push(`[web-token] ${k} 值漂移:原型=${v} 官网=${wv[k]} —— web/assets/tokens.css 需同步原型主题 A`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ④ web 硬编码色扫描 ─────────────────────────────────────
|
||||
{
|
||||
const ALLOW_HEX = new Set(['fff', 'ffffff', '1677ff']); // 白 + 支付宝品牌蓝(同原型 check-ds 口径)
|
||||
const SKIP = new Set(['web/assets/tokens.css']); // token 定义位
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
for (const name of readdirSync(join(ROOT, dir))) {
|
||||
const rel = `${dir}/${name}`;
|
||||
if (rel === 'web/dist' || name === 'node_modules') continue;
|
||||
const st = statSync(join(ROOT, rel));
|
||||
if (st.isDirectory()) walk(rel);
|
||||
else if (/\.(njk|css|js)$/.test(name) && !SKIP.has(rel)) files.push(rel);
|
||||
}
|
||||
};
|
||||
walk('web');
|
||||
for (const f of files) {
|
||||
const lines = read(f).split('\n');
|
||||
lines.forEach((line, i) => {
|
||||
if (line.includes('ds-allow')) return;
|
||||
for (const [hex] of line.matchAll(/#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g)) {
|
||||
const v = hex.slice(1).toLowerCase();
|
||||
if (!ALLOW_HEX.has(v)) problems.push(`[web-hex] ${f}:${i + 1} 硬编码颜色 ${hex} —— 改用 var(--token)(web/assets/tokens.css),确需保留加 ds-allow 注释`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 汇总 ───────────────────────────────────────────────────
|
||||
const line = '='.repeat(60);
|
||||
if (problems.length) {
|
||||
console.error(line);
|
||||
console.error(`✗ L1 真相源同源闸未过(${problems.length} 处):`);
|
||||
for (const p of problems) console.error(' · ' + p);
|
||||
console.error(line);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(line);
|
||||
console.log('✓ 通过:L1 真相源同源(tokens 快照 / icons / 官网 token 值 / web 无硬编码色)');
|
||||
console.log(line);
|
||||
@@ -3,6 +3,9 @@
|
||||
// fidelity.mjs(自动 diff 闸)与 ds-compare.mjs(人工 montage 目检)共用此表,
|
||||
// 保证「每一屏 + 外壳都跟原型比」(规则 2)。屏随 Phase 1/2 照原型重建后逐个入册。
|
||||
//
|
||||
// 本表只登记 design/CONTRACT.md 块 4 中「真相源 = 同步」的屏,两处互为镜像;
|
||||
// 快照 / 代码先行 态屏不入册(分层规则见 CLAUDE.md「设计真相源分层」)。
|
||||
//
|
||||
// 字段:
|
||||
// html 原型 html(仓库根相对路径)
|
||||
// prefix Flutter golden 文件前缀 → client/test/golden/goldens/<prefix>_<theme>.png
|
||||
|
||||
@@ -49,7 +49,7 @@ permalink: /checkout/
|
||||
<div class="radio"></div>
|
||||
</div>
|
||||
<div class="pm off">
|
||||
<div class="pic" style="background:#8A94A6"><svg class="ic"><use href="#i-ic11"/></svg></div>
|
||||
<div class="pic" style="background:var(--faint)"><svg class="ic"><use href="#i-ic11"/></svg></div>
|
||||
<div class="nm"><b>对公转账</b><span>需要合同与对公收款请<a href="mailto:{{ site.support.email }}">联系我们</a></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
<div class="detect-info">
|
||||
<div class="detect-eyebrow">已识别你的设备 · 为你推荐</div>
|
||||
<div class="detect-title">
|
||||
<b id="detect-name">Web 版</b>
|
||||
<span id="detect-desc">已自动识别你的设备,推荐以下版本。</span>
|
||||
<span class="detect-ver version-badge">v{{ changelog[0].version }}</span>
|
||||
</div>
|
||||
<a class="btn primary" id="detect-btn" href="{{ site.appBaseUrl }}/app/"><span>立即使用</span></a>
|
||||
<span class="detect-desc" id="detect-desc">浏览器直接访问,功能完整,无需下载安装。</span>
|
||||
</div>
|
||||
<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 },
|
||||
|
||||