Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e10048e60 | |||
| 533f36da00 | |||
| c49ee3b1df | |||
| c9217601a1 | |||
| 99ceaa87bb | |||
| 86611dc9ee | |||
| 59d56d7e30 |
@@ -5,6 +5,31 @@ 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.4] - 2026-07-05
|
||||
|
||||
### 新功能
|
||||
- 手机端全面改版:底部「库存 / 入库 / 出库 / 财务 / 我的」五个标签页替代抽屉菜单,
|
||||
「我的」汇总往来、基础数据、盘点、用户、授权、设备、设置、关于等入口。
|
||||
- 手机端所有列表按移动风格重排:指标卡可点击筛选,卡片流带状态图标徽章,
|
||||
详情、筛选、表单全部改为底部弹层,单手操作更顺。
|
||||
- 手机端横滑手势:首页左右滑切换标签页(带滑动动画),二级页面右滑返回,
|
||||
弹层打开时右滑关闭弹层。
|
||||
- 所有列表支持下拉刷新。
|
||||
- 库存商品编辑抽屉新增「打印标签」按钮,与入库管理打印标签一致。
|
||||
- 从 App 内点击官网链接(如查看套餐价格)自动免登录,无需再输一遍账号密码。
|
||||
- 手机购买套餐直接拉起支付宝 App 付款,不再停留在扫码页。
|
||||
|
||||
### 改进
|
||||
- 日期选择全面统一:点开为扁平下拉面板(不再弹窗冻结页面),支持直接键入日期,
|
||||
也可三列滚轮选择;日期范围为起止双窗格并排,输入与滚轮双向联动。
|
||||
- 抽屉内点击操作按钮后原地刷新,不再自动收起抽屉;仅点击抽屉外区域才关闭。
|
||||
- 在线购买完成后套餐页默认停在「高级版·年付」。
|
||||
- 关于页「检查更新」按钮与反馈按钮同规格对齐。
|
||||
|
||||
### 修复
|
||||
- 手机端通知铃菜单不再悬空漂移,紧贴顶栏弹出。
|
||||
- 本地测试构建的版本号显示不正确(含 iOS),并会误弹更新提示。
|
||||
|
||||
## [1.1.3] - 2026-07-04
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
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.5] - 2026-07-04
|
||||
|
||||
### 新功能
|
||||
- 新增 /sso 免登录落地页:从 App 跳转官网自动登录(凭 App 签发的一次性票据换取
|
||||
登录态后直达目标页),登录态与 Web 版 App 共享
|
||||
|
||||
## [1.1.4] - 2026-07-04
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -95,8 +95,8 @@ class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
|
||||
final downloadUrls =
|
||||
rawUrls.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||||
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final hasUpdate = _isNewer(latestVersion, info.version);
|
||||
final current = (await currentAppVersion()).substring(1); // 去 v 前缀
|
||||
final hasUpdate = _isNewer(latestVersion, current);
|
||||
|
||||
return AppUpdateInfo(
|
||||
latestVersion: latestVersion,
|
||||
@@ -123,16 +123,32 @@ class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
|
||||
}
|
||||
|
||||
List<int> _parse(String v) {
|
||||
final parts = v.split('.').map((s) => int.tryParse(s) ?? 0).toList();
|
||||
// 每段取前导数字(容忍 1.1.4-dev / 1.1.4+7 等本地构建后缀)
|
||||
final parts = v.split('.').map((s) {
|
||||
final m = RegExp(r'^\d+').firstMatch(s);
|
||||
return m == null ? 0 : int.parse(m.group(0)!);
|
||||
}).toList();
|
||||
while (parts.length < 3) parts.add(0);
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 版本号 Provider(供状态栏 / 门店信息面板使用)──────────
|
||||
final appVersionProvider = FutureProvider<String>((ref) async {
|
||||
// ── 当前版本(单一真相)─────────────────────────────────────
|
||||
// 优先编译期 APP_VERSION dart-define(CI 与本地脚本都传;本地构建不改
|
||||
// pubspec,PackageInfo 会是旧值);未定义时回退 PackageInfo。
|
||||
const _definedVersion = String.fromEnvironment('APP_VERSION');
|
||||
|
||||
Future<String> currentAppVersion() async {
|
||||
if (_definedVersion.isNotEmpty) {
|
||||
return _definedVersion.startsWith('v') ? _definedVersion : 'v$_definedVersion';
|
||||
}
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return 'v${info.version}';
|
||||
}
|
||||
|
||||
// ── 版本号 Provider(供状态栏 / 门店信息面板使用)──────────
|
||||
final appVersionProvider = FutureProvider<String>((ref) async {
|
||||
return currentAppVersion();
|
||||
});
|
||||
|
||||
// ── 打开下载链接工具函数 ────────────────────────────────────
|
||||
|
||||
@@ -560,6 +560,11 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
||||
shrinkWrap: true,
|
||||
emptyText: '没有匹配的流水记录',
|
||||
toolbar: toolbar,
|
||||
// 下拉刷新 = 桌面「刷新」(_reload 连带 KPI/汇总/趋势)
|
||||
onRefresh: () async {
|
||||
_reload();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
},
|
||||
mobileCards: flows.map((r) => _flowCard(t, r)).toList(),
|
||||
columns: const [
|
||||
DsColumn('date', '日期'),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
@@ -41,7 +40,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
// 库存状态前端筛选(派生自 qty vs 安全库存):全部/在售/预警/缺货
|
||||
String _statusFilter = '全部';
|
||||
Set<String>? _hiddenCols; // null = 尚未载入本地存档
|
||||
bool _mobileTableView = false; // 移动端:false=卡片列表,true=表格(横向滚动)
|
||||
|
||||
static const _screenId = 'inventory_list';
|
||||
static const _statusOptions = ['全部', '在售', '预警', '缺货'];
|
||||
@@ -812,7 +810,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
ref.read(inventoryListProvider.notifier).setPageSize(s),
|
||||
toolbar: Builder(builder: (ctx) {
|
||||
final isMobile = ctx.isMobile;
|
||||
void doExport() => _exportInventory(filteredItems);
|
||||
|
||||
// 搜索合一(用户口径):名称/拼音/编码同框,回车触发
|
||||
// (后端 keyword 本就 LIKE name/code/拼音/首字母)。
|
||||
@@ -824,11 +821,10 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
.setKeyword(v.trim()),
|
||||
);
|
||||
|
||||
final canCheck = !WriteGuard.isReadonly(ref);
|
||||
|
||||
if (isMobile) {
|
||||
// 移动端:MSearchRow(原型 .m-search + 详搜钮)独占一行
|
||||
// + 紧凑图标操作行(含 表格/列表 切换)
|
||||
//(盘点/导出/列设置/刷新工具行 2026-07-04 拍板移除,
|
||||
// 刷新改下拉手势,盘点入口在「我的」)
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -842,62 +838,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
onFilterTap: () => _openMobileFilterSheet(
|
||||
specOptions, seriesOptions),
|
||||
),
|
||||
const SizedBox(height: AppDims.sp2),
|
||||
Row(
|
||||
children: [
|
||||
if (canCheck)
|
||||
WriteGuard(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
context.go('/inventory/check'),
|
||||
icon: const Icon(
|
||||
LucideIcons.clipboardCheck,
|
||||
size: 16),
|
||||
label: const Text('盘点'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: _mobileTableView ? '卡片视图' : '表格视图',
|
||||
icon: Icon(
|
||||
_mobileTableView
|
||||
? LucideIcons.layoutList
|
||||
: LucideIcons.rows3,
|
||||
size: 20),
|
||||
onPressed: () => setState(() =>
|
||||
_mobileTableView = !_mobileTableView),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '导出',
|
||||
icon: const Icon(LucideIcons.download,
|
||||
size: 20),
|
||||
onPressed: doExport,
|
||||
),
|
||||
Builder(
|
||||
builder: (bctx) => IconButton(
|
||||
tooltip: '列设置',
|
||||
icon: const Icon(LucideIcons.alignLeft,
|
||||
size: 20),
|
||||
onPressed: () => _openColMenu(bctx),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '刷新',
|
||||
icon: const Icon(LucideIcons.refreshCw,
|
||||
size: 20),
|
||||
onPressed: () => ref
|
||||
.read(inventoryListProvider.notifier)
|
||||
.reload(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -912,9 +852,13 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
],
|
||||
);
|
||||
}),
|
||||
mobileCards: _mobileTableView
|
||||
? null
|
||||
: filteredItems.map(_inventoryCard).toList(),
|
||||
mobileCards: filteredItems.map(_inventoryCard).toList(),
|
||||
// 下拉刷新 = 桌面「刷新」(reload 保留旧内容叠进度)
|
||||
onRefresh: () async {
|
||||
ref.read(inventoryListProvider.notifier).reload();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 600));
|
||||
},
|
||||
emptyText: '没有匹配的商品 · 试试调整筛选或搜索',
|
||||
columns: visibleCols.map((c) {
|
||||
final Widget? header = switch (c.key) {
|
||||
|
||||
@@ -147,6 +147,12 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
pagerInfoText: '共 ${result.total} 个往来单位',
|
||||
emptyText: '没有匹配的往来单位 · 试试调整筛选或搜索',
|
||||
toolbar: _toolbar(mobile, partners),
|
||||
// 下拉刷新 = 桌面「刷新」
|
||||
onRefresh: () async {
|
||||
ref.read(partnersScreenProvider.notifier).reload();
|
||||
ref.invalidate(partnerBalancesProvider);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
},
|
||||
mobileCards: partners.map((p) => _card(p, balances)).toList(),
|
||||
columns: const [
|
||||
DsColumn('name', '名称'),
|
||||
|
||||
@@ -114,6 +114,16 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
_ => '',
|
||||
};
|
||||
|
||||
/// 下拉刷新(全 tab 通用):档案/介绍/系列/规格/仓库 一并重拉。
|
||||
Future<void> _pullRefresh() async {
|
||||
ref.read(productListProvider.notifier).reload();
|
||||
ref.invalidate(productDescriptionDocListProvider);
|
||||
ref.read(productSeriesListProvider.notifier).reload();
|
||||
ref.read(productSpecListProvider.notifier).reload();
|
||||
ref.read(warehouseListProvider.notifier).reload();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
@@ -370,6 +380,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
onRefresh: _pullRefresh,
|
||||
mobileCards: items
|
||||
.map((p) => MobileListCard(
|
||||
onTap: () => showProductEditorDrawer(context,
|
||||
@@ -470,6 +481,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
onRefresh: _pullRefresh,
|
||||
mobileCards: filtered
|
||||
.map((it) => MobileListCard(
|
||||
title: Text(it.title),
|
||||
@@ -695,6 +707,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
onExport: onExport,
|
||||
mobileAdd: true,
|
||||
),
|
||||
onRefresh: _pullRefresh,
|
||||
mobileCards: filtered
|
||||
.map((r) => MobileListCard(
|
||||
title: Text(r.name),
|
||||
@@ -775,6 +788,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
onChanged: () => setState(() {}),
|
||||
mobileAdd: true,
|
||||
),
|
||||
onRefresh: _pullRefresh,
|
||||
mobileCards: filtered
|
||||
.map((w) => MobileListCard(
|
||||
title: Text(w.name),
|
||||
|
||||
@@ -122,6 +122,11 @@ class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
setState(() {
|
||||
_newExpiresAt = st.expiresAt;
|
||||
_phase = _Phase.success;
|
||||
// 购买完成后,选择页默认落到 高级版·年付(特惠只能买一次,
|
||||
// 后续复购/升级的主推档位);本次若是特惠单,本店已享用置灰
|
||||
if (_isPromo) _promoUsed = true;
|
||||
_tabIdx = 2;
|
||||
_cycleIdx = 0;
|
||||
});
|
||||
} else if (manual) {
|
||||
showDsToast(context, '尚未确认到账,支付完成后通常数秒内到账');
|
||||
|
||||
@@ -174,6 +174,8 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
Expanded(
|
||||
child: DsTable(
|
||||
pagerInfoText: '共 ${filtered.length} 名成员',
|
||||
// 下拉刷新 = 桌面「刷新」
|
||||
onRefresh: () => ref.read(userListProvider.notifier).reload(),
|
||||
emptyText: '没有匹配的成员',
|
||||
toolbar: _toolbar(mobile),
|
||||
columns: const [
|
||||
|
||||
@@ -411,6 +411,11 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
supplierOptions: supplierOptions,
|
||||
selectedSupplierId: selectedSupplierId,
|
||||
),
|
||||
// 下拉刷新 = 桌面「刷新」
|
||||
onRefresh: () async {
|
||||
ref.read(stockInListProvider.notifier).reload();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
},
|
||||
mobileCards:
|
||||
orders.map((o) => _orderCard(context, o)).toList(),
|
||||
columns: _colDefs
|
||||
|
||||
@@ -406,6 +406,11 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
// 下拉刷新 = 桌面「刷新」
|
||||
onRefresh: () async {
|
||||
ref.read(stockOutListProvider.notifier).reload();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
},
|
||||
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -45,6 +45,9 @@ class DsTable extends StatefulWidget {
|
||||
// 仅文案分页(原型 partners/products 的 .pager 只有一行 span 文案,无翻页控件);
|
||||
// 与 total 互斥,total 为空且此值非空时生效。
|
||||
final String? pagerInfoText;
|
||||
|
||||
/// 窄屏卡片流下拉刷新(等价桌面工具栏「刷新」,2026-07-04 拍板);null 不启用。
|
||||
final Future<void> Function()? onRefresh;
|
||||
// 内嵌收缩模式(多区块滚动页里的表格卡,如 devices/settings):
|
||||
// 不用 Expanded 撑满、表格不带内部滚动,高度随内容。
|
||||
final bool shrinkWrap;
|
||||
@@ -62,6 +65,7 @@ class DsTable extends StatefulWidget {
|
||||
this.onPageSizeChanged,
|
||||
this.mobileCards,
|
||||
this.pagerInfoText,
|
||||
this.onRefresh,
|
||||
this.shrinkWrap = false,
|
||||
});
|
||||
|
||||
@@ -146,13 +150,27 @@ class _DsTableState extends State<DsTable> {
|
||||
|
||||
Widget _buildMobile(dynamic t) {
|
||||
final cards = widget.mobileCards!;
|
||||
if (cards.isEmpty) return _empty(t);
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: cards.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (_, i) => cards[i],
|
||||
);
|
||||
final Widget list = cards.isEmpty
|
||||
? (widget.onRefresh == null
|
||||
? _empty(t)
|
||||
// 空态也要可下拉:套一个总是可滚的 ListView
|
||||
: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 60),
|
||||
child: _empty(t)),
|
||||
],
|
||||
))
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: cards.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (_, i) => cards[i],
|
||||
);
|
||||
if (widget.onRefresh == null) return list;
|
||||
return RefreshIndicator(onRefresh: widget.onRefresh!, child: list);
|
||||
}
|
||||
|
||||
/// 收缩版卡片流(外层页面自带滚动)。
|
||||
|
||||
@@ -11,7 +11,9 @@ class NotificationBell extends StatelessWidget {
|
||||
final t = context.tokens;
|
||||
return PopupMenuButton<void>(
|
||||
tooltip: '通知',
|
||||
offset: const Offset(0, 40),
|
||||
// under 已定位在按钮正下方,只留小间隙贴着顶栏(大偏移在手机顶栏
|
||||
// 加 safe-area inset 后会悬空漂进内容区——2026-07-04 用户反馈)
|
||||
offset: const Offset(0, 8),
|
||||
position: PopupMenuPosition.under,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
color: t.surface,
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../core/config/app_config.dart';
|
||||
import '../core/utils/dialog_util.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import '../core/theme/app_chrome.g.dart';
|
||||
import '../core/theme/app_dims.g.dart';
|
||||
@@ -20,11 +21,14 @@ import '../models/product.dart';
|
||||
import '../models/product_image.dart';
|
||||
import '../providers/product_option_provider.dart';
|
||||
import '../providers/product_provider.dart';
|
||||
import '../providers/shop_provider.dart' show shopInfoProvider;
|
||||
import 'ds/ds_atoms.dart';
|
||||
import 'ds/grid_combo_cell.dart';
|
||||
import 'ds/m_sheet.dart';
|
||||
import 'searchable_option_field.dart';
|
||||
import 'fullscreen_image_viewer.dart';
|
||||
import '../core/utils/label_data.dart';
|
||||
import 'label_preview_dialog.dart';
|
||||
import 'write_guard.dart';
|
||||
import 'ds/ds_toast.dart';
|
||||
|
||||
@@ -184,6 +188,14 @@ class _ProductEditorDrawerState extends ConsumerState<_ProductEditorDrawer> {
|
||||
onPressed: p == null || p.publicId.isEmpty
|
||||
? null
|
||||
: () => context.push('/product/${p.publicId}')),
|
||||
// 打印标签(同入库管理打印标签;移动端无打印——用户拍板)
|
||||
if (!context.isMobile) ...[
|
||||
const SizedBox(width: 10),
|
||||
DsButton('打印标签',
|
||||
icon: LucideIcons.printer,
|
||||
small: true,
|
||||
onPressed: p == null ? null : () => _printLabel(p)),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
@@ -615,6 +627,31 @@ class _ProductEditorDrawerState extends ConsumerState<_ProductEditorDrawer> {
|
||||
}
|
||||
|
||||
// ── 保存:介绍正文 +(选了的话)介绍库文档 ──
|
||||
/// 打印标签(同入库管理打印标签:QR + 品牌/型号/版本 + 门店信息)。
|
||||
Future<void> _printLabel(Product p) async {
|
||||
final qrBytes =
|
||||
await ref.read(productRepositoryProvider).getQRCodeBytes(p.id);
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
if (!mounted) return;
|
||||
final label = LabelData(
|
||||
productId: p.id,
|
||||
qrBytes: qrBytes,
|
||||
name: p.name,
|
||||
code: p.code,
|
||||
series: (p.series ?? '').isEmpty ? null : p.series,
|
||||
spec: (p.spec ?? '').isEmpty ? null : p.spec,
|
||||
remark: (p.remark ?? '').isEmpty ? null : p.remark,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
);
|
||||
if (!mounted) return;
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (_) => LabelPreviewDialog(labels: [label]),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
|
||||
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
@@ -1,17 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# local_test.sh — 本地编译一份「指向线上后端」的 macOS app 并启动,用于发版前自测。
|
||||
# local_test.sh — 本地编译一份「指向线上后端」的 app 用于发版前自测。
|
||||
#
|
||||
# 用法:
|
||||
# sh scripts/local_test.sh # 编译 + 启动
|
||||
# sh scripts/local_test.sh --no-open # 只编译不启动
|
||||
# sh scripts/local_test.sh # 编译 macOS + 启动
|
||||
# sh scripts/local_test.sh --no-open # 只编译 macOS 不启动
|
||||
# sh scripts/local_test.sh --ios # 编译 iOS 并装到 USB iPhone(开发签名)
|
||||
#
|
||||
# 后端固定指向生产 https://jiu.51yanmei.com(与 CI compile-macos.sh 一致;
|
||||
# 2026-07-03 备案通过已回切 https 域名)。
|
||||
# 后端固定指向生产 https://jiu.51yanmei.com(与 CI compile 脚本一致)。
|
||||
# 版本号 = 最新 client-v* tag 的 patch+1 加 -dev 后缀(如 v1.1.4-dev),
|
||||
# App 内显示走 APP_VERSION dart-define(update_provider.currentAppVersion),
|
||||
# 与 CI 同一读取链路——本地构建不再显示 pubspec 旧版本(2026-07-04 修复)。
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
|
||||
# 版本号:取最新 client-v* tag 的 patch+1,纯展示用,取不到则回退 0.0.0-dev。
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
@@ -20,19 +22,52 @@ if [ -n "$LATEST_TAG" ]; then
|
||||
BASE_VER="${LATEST_TAG#client-v}"
|
||||
MAJOR_MINOR="${BASE_VER%.*}"
|
||||
PATCH="${BASE_VER##*.}"
|
||||
VER="${MAJOR_MINOR}.$((PATCH + 1))"
|
||||
VER="${MAJOR_MINOR}.$((PATCH + 1))-dev"
|
||||
else
|
||||
VER="0.0.0-dev"
|
||||
fi
|
||||
|
||||
DEFINES=(
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com"
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com"
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
)
|
||||
|
||||
if [ "${1:-}" = "--ios" ]; then
|
||||
# ── iOS:开发签名构建 + devicectl 装到 USB iPhone ──
|
||||
# Release 配置是 CI 的 Manual+App Store 签名,本机没有分发证书,
|
||||
# 故 xcodebuild 用 Automatic + Apple Development 覆盖(不改工程文件)。
|
||||
echo "==> 编译 iOS(线上后端,版本 v${VER})"
|
||||
cd client
|
||||
flutter build ios --release --config-only "${DEFINES[@]}"
|
||||
cd ios
|
||||
CD_ID="$(xcrun devicectl list devices 2>/dev/null | grep 'available (paired)' | grep 'iPhone' | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -1 || true)"
|
||||
if [ -z "$CD_ID" ]; then
|
||||
echo "!! 未检测到已配对 iPhone(插线并解锁后重试)" >&2
|
||||
exit 1
|
||||
fi
|
||||
xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release \
|
||||
-destination "generic/platform=iOS" \
|
||||
CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=BYL4KQHMTN \
|
||||
CODE_SIGN_IDENTITY="Apple Development" PROVISIONING_PROFILE_SPECIFIER= \
|
||||
-allowProvisioningUpdates -allowProvisioningDeviceRegistration build \
|
||||
| grep -E "BUILD (SUCCEEDED|FAILED)"
|
||||
APP_PATH="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path '*Release-iphoneos/Runner.app' -maxdepth 6 2>/dev/null | head -1)"
|
||||
if [ -z "$APP_PATH" ]; then
|
||||
echo "!! 找不到构建产物 Runner.app" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> 安装到 iPhone($CD_ID)"
|
||||
xcrun devicectl device install app --device "$CD_ID" "$APP_PATH" | grep -m1 databaseSequenceNumber || true
|
||||
echo "==> 完成:解锁手机打开 岩美酒库(版本 v${VER})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
APP="client/build/macos/Build/Products/Release/jiu_client.app"
|
||||
|
||||
echo "==> 编译 macOS app(线上后端,版本 v${VER})"
|
||||
cd client
|
||||
flutter build macos --release \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
flutter build macos --release "${DEFINES[@]}"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo "==> 产物:$APP"
|
||||
|
||||