feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服 pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验; 登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段, 已记 CONTRACT,screens.mjs 留存根) - ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/ DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态 - 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、 h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、 BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast) - 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后 失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览 - 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/ SelectProductDialog/tabStateProvider - golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打; 修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -157,8 +157,10 @@ class ApiClient {
|
||||
return handler.next(e);
|
||||
}
|
||||
|
||||
if (e.response?.statusCode == 401 && (_refreshToken ?? '').isNotEmpty) {
|
||||
debugPrint('[ApiClient] got 401 on ${e.requestOptions.path}, trying refresh...');
|
||||
if (e.response?.statusCode == 401 &&
|
||||
(_refreshToken ?? '').isNotEmpty) {
|
||||
debugPrint(
|
||||
'[ApiClient] got 401 on ${e.requestOptions.path}, trying refresh...');
|
||||
final newToken = await _refreshAccessToken();
|
||||
if (newToken == null) {
|
||||
return handler.next(e);
|
||||
@@ -172,7 +174,8 @@ class ApiClient {
|
||||
return handler.next(retryErr is DioException ? retryErr : e);
|
||||
}
|
||||
} else if (e.response?.statusCode == 401) {
|
||||
debugPrint('[ApiClient] got 401 on ${e.requestOptions.path}, no refresh token');
|
||||
debugPrint(
|
||||
'[ApiClient] got 401 on ${e.requestOptions.path}, no refresh token');
|
||||
}
|
||||
return handler.next(e);
|
||||
},
|
||||
|
||||
@@ -101,7 +101,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}
|
||||
|
||||
Future<void> login(AuthUser user) async {
|
||||
debugPrint('[Auth] login() called, username=${user.username} shopId=${user.shopId}');
|
||||
debugPrint(
|
||||
'[Auth] login() called, username=${user.username} shopId=${user.shopId}');
|
||||
final gen = ++_sessionGen;
|
||||
await _runStorage(() async {
|
||||
if (gen != _sessionGen) return; // 期间又发生 login/logout → 放弃这次写盘
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/license.dart';
|
||||
@@ -113,11 +114,14 @@ class LicenseCopy {
|
||||
static Color bannerColor(String phase) {
|
||||
switch (phase) {
|
||||
case 'locked':
|
||||
return const Color(0xFFD14343);
|
||||
return const Color(
|
||||
0xFFD14343); // ds-ignore: 授权紧迫度专用深色,主题无关(承白字,历史分叉已集中)
|
||||
case 'readonly':
|
||||
return const Color(0xFFB71C1C); // 深红,强调即将彻底锁定
|
||||
return const Color(
|
||||
0xFFB71C1C); // 深红,强调即将彻底锁定 // ds-ignore: 授权紧迫度专用深色,主题无关(承白字,历史分叉已集中)
|
||||
case 'grace':
|
||||
return const Color(0xFFE65100); // 深橙
|
||||
return const Color(
|
||||
0xFFE65100); // 深橙 // ds-ignore: 授权紧迫度专用深色,主题无关(承白字,历史分叉已集中)
|
||||
default:
|
||||
return Colors.transparent;
|
||||
}
|
||||
@@ -127,12 +131,15 @@ class LicenseCopy {
|
||||
static Color phaseColor(String phase) {
|
||||
switch (phase) {
|
||||
case 'grace':
|
||||
return const Color(0xFFE08E00);
|
||||
return const Color(
|
||||
0xFFE08E00); // ds-ignore: 授权紧迫度专用深色,主题无关(承白字,历史分叉已集中)
|
||||
case 'readonly':
|
||||
case 'locked':
|
||||
return const Color(0xFFD14343);
|
||||
return const Color(
|
||||
0xFFD14343); // ds-ignore: 授权紧迫度专用深色,主题无关(承白字,历史分叉已集中)
|
||||
default:
|
||||
return const Color(0xFF2E8B57);
|
||||
return const Color(
|
||||
0xFF2E8B57); // ds-ignore: 授权紧迫度专用深色,主题无关(承白字,历史分叉已集中)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,12 +147,12 @@ class LicenseCopy {
|
||||
static IconData phaseIcon(String phase) {
|
||||
switch (phase) {
|
||||
case 'grace':
|
||||
return Icons.warning_amber_rounded;
|
||||
return LucideIcons.triangleAlert;
|
||||
case 'readonly':
|
||||
case 'locked':
|
||||
return Icons.error_outline;
|
||||
return LucideIcons.circleAlert;
|
||||
default:
|
||||
return Icons.verified_user_outlined;
|
||||
return LucideIcons.shieldCheck;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,9 @@ class ErrorReporter {
|
||||
String? stack;
|
||||
if (stackTrace != null) {
|
||||
final raw = stackTrace.toString();
|
||||
stack = raw.length > _maxStackLength ? raw.substring(0, _maxStackLength) : raw;
|
||||
stack = raw.length > _maxStackLength
|
||||
? raw.substring(0, _maxStackLength)
|
||||
: raw;
|
||||
}
|
||||
|
||||
await _client.post('/public/errors', data: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
// Keep the server socket alive for the lifetime of the process.
|
||||
// The OS releases the port automatically when the process exits.
|
||||
// ignore: unused_element — 只写不读是刻意的:持有引用防 GC 关闭端口锁
|
||||
ServerSocket? _server;
|
||||
|
||||
// Port unique to this app; chosen from the private range 49152–65535.
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../screens/products/product_detail_screen.dart';
|
||||
import '../../screens/public/public_product_screen.dart';
|
||||
import '../../screens/public/public_shop_products_screen.dart';
|
||||
import '../../screens/settings/settings_screen.dart';
|
||||
import '../../screens/settings/users_screen.dart';
|
||||
import '../../screens/about/about_screen.dart';
|
||||
import '../../screens/devices/device_management_screen.dart';
|
||||
import '../auth/auth_state.dart';
|
||||
@@ -163,7 +164,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
StatefulShellBranch(routes: [
|
||||
GoRoute(
|
||||
path: '/products',
|
||||
pageBuilder: (_, __) => _noTransition(const ProductsScreen())),
|
||||
// ?tab=intro → 商品介绍 tab(对齐原型 products.html?tab=intro)
|
||||
pageBuilder: (_, state) => _noTransition(ProductsScreen(
|
||||
initialTab:
|
||||
state.uri.queryParameters['tab'] == 'intro' ? 1 : 0))),
|
||||
GoRoute(
|
||||
path: '/products/:id',
|
||||
pageBuilder: (_, state) => _noTransition(ProductDetailScreen(
|
||||
@@ -176,7 +180,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
pageBuilder: (_, __) =>
|
||||
_noTransition(const DeviceManagementScreen())),
|
||||
]),
|
||||
// 7 系统设置
|
||||
// 7 系统设置(面板:门店信息/用户管理/编号规则/授权兑换券/偏好设置)
|
||||
StatefulShellBranch(routes: [
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
@@ -185,13 +189,16 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||
'shop': 0,
|
||||
'users': 1,
|
||||
'number': 2,
|
||||
'system': 3,
|
||||
'license': 4,
|
||||
'import': 5,
|
||||
'license': 3,
|
||||
'pref': 4,
|
||||
};
|
||||
final tab = tabIndex[state.uri.queryParameters['tab']] ?? 0;
|
||||
return _noTransition(SettingsScreen(initialTab: tab));
|
||||
}),
|
||||
// 用户管理独立页(照原型 users.html)
|
||||
GoRoute(
|
||||
path: '/settings/users',
|
||||
pageBuilder: (_, __) => _noTransition(const UsersScreen())),
|
||||
]),
|
||||
// 8 关于我们
|
||||
StatefulShellBranch(routes: [
|
||||
|
||||
@@ -7,6 +7,18 @@ class LoginHistoryStorage {
|
||||
static const int _maxItems = 5;
|
||||
static const String _hotelKey = 'login_history_hotels';
|
||||
static const String _usernameKey = 'login_history_usernames';
|
||||
static const String _rememberKey = 'login_remember';
|
||||
|
||||
/// 「记住我」偏好(登录页复选框,默认开)。关闭时登录成功不记录账号历史。
|
||||
static Future<bool> getRemember() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_rememberKey) ?? true;
|
||||
}
|
||||
|
||||
static Future<void> setRemember(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_rememberKey, value);
|
||||
}
|
||||
|
||||
static Future<List<String>> getHotelCodes() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/// 字体真相源。
|
||||
///
|
||||
/// 等宽字体镜像原型 tokens.css `--font-mono` 首选 **JetBrains Mono**,
|
||||
/// 以资产打包(pubspec `JetBrainsMono` 400/700)——原型截图、golden 测试、
|
||||
/// 三端真机渲染完全同源。历史教训:曾写 CSS 泛型名 `'monospace'`,Flutter
|
||||
/// **不解析**并静默回退(真机无衬线、golden 里 tofu 方块),勿走回头路。
|
||||
/// CJK 字形不在 JetBrains Mono 内,回退链兜底系统等宽/默认字体。
|
||||
class AppFonts {
|
||||
const AppFonts._();
|
||||
|
||||
static const String mono = 'JetBrainsMono';
|
||||
static const List<String> monoFallback = [
|
||||
'Menlo',
|
||||
'SF Mono',
|
||||
'Consolas',
|
||||
'Roboto Mono',
|
||||
'monospace',
|
||||
];
|
||||
}
|
||||
@@ -2,61 +2,193 @@ import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class AppTokens extends ThemeExtension<AppTokens> {
|
||||
final Color page, bg, surface, border, borderSubtle, text, heading, title, muted, faint;
|
||||
final Color page,
|
||||
bg,
|
||||
surface,
|
||||
border,
|
||||
borderSubtle,
|
||||
text,
|
||||
heading,
|
||||
title,
|
||||
muted,
|
||||
faint;
|
||||
final Color primary, primaryDark, onPrimary, brand400, brand50, accent;
|
||||
final Color success, danger, warn, infoSoft, okSoft, accentSoft, successBg, dangerBg, warnBg;
|
||||
final Color success,
|
||||
danger,
|
||||
warn,
|
||||
infoSoft,
|
||||
okSoft,
|
||||
accentSoft,
|
||||
successBg,
|
||||
dangerBg,
|
||||
warnBg;
|
||||
final Color thBg, rowHover, shadow;
|
||||
final Color topBg, topFg, topFaint, topBorder, topControl, topMarkBg, topMarkFg;
|
||||
final Color sideBg, sideFg, sideMuted, sideActiveBg, sideActiveFg, sideBorder, sideHover;
|
||||
final Color topBg,
|
||||
topFg,
|
||||
topFaint,
|
||||
topBorder,
|
||||
topControl,
|
||||
topMarkBg,
|
||||
topMarkFg;
|
||||
final Color sideBg,
|
||||
sideFg,
|
||||
sideMuted,
|
||||
sideActiveBg,
|
||||
sideActiveFg,
|
||||
sideBorder,
|
||||
sideHover;
|
||||
final Color menuUserBg, menuUserFg, menuUserHover;
|
||||
final Color toastBg; // 原型 --toast-bg(基础 :root 共用,主题不变)
|
||||
final bool isDark;
|
||||
|
||||
const AppTokens({
|
||||
required this.page, required this.bg, required this.surface, required this.border,
|
||||
required this.borderSubtle, required this.text, required this.heading, required this.title,
|
||||
required this.muted, required this.faint, required this.primary, required this.primaryDark,
|
||||
required this.onPrimary, required this.brand400, required this.brand50, required this.accent,
|
||||
required this.success, required this.danger, required this.warn, required this.infoSoft,
|
||||
required this.okSoft, required this.accentSoft, required this.successBg, required this.dangerBg,
|
||||
required this.warnBg, required this.thBg, required this.rowHover, required this.shadow,
|
||||
required this.topBg, required this.topFg, required this.topFaint, required this.topBorder,
|
||||
required this.topControl, required this.topMarkBg, required this.topMarkFg, required this.sideBg,
|
||||
required this.sideFg, required this.sideMuted, required this.sideActiveBg, required this.sideActiveFg,
|
||||
required this.sideBorder, required this.sideHover, required this.menuUserBg, required this.menuUserFg,
|
||||
required this.menuUserHover, required this.isDark,
|
||||
required this.page,
|
||||
required this.bg,
|
||||
required this.surface,
|
||||
required this.border,
|
||||
required this.borderSubtle,
|
||||
required this.text,
|
||||
required this.heading,
|
||||
required this.title,
|
||||
required this.muted,
|
||||
required this.faint,
|
||||
required this.primary,
|
||||
required this.primaryDark,
|
||||
required this.onPrimary,
|
||||
required this.brand400,
|
||||
required this.brand50,
|
||||
required this.accent,
|
||||
required this.success,
|
||||
required this.danger,
|
||||
required this.warn,
|
||||
required this.infoSoft,
|
||||
required this.okSoft,
|
||||
required this.accentSoft,
|
||||
required this.successBg,
|
||||
required this.dangerBg,
|
||||
required this.warnBg,
|
||||
required this.thBg,
|
||||
required this.rowHover,
|
||||
required this.shadow,
|
||||
required this.topBg,
|
||||
required this.topFg,
|
||||
required this.topFaint,
|
||||
required this.topBorder,
|
||||
required this.topControl,
|
||||
required this.topMarkBg,
|
||||
required this.topMarkFg,
|
||||
required this.sideBg,
|
||||
required this.sideFg,
|
||||
required this.sideMuted,
|
||||
required this.sideActiveBg,
|
||||
required this.sideActiveFg,
|
||||
required this.sideBorder,
|
||||
required this.sideHover,
|
||||
required this.menuUserBg,
|
||||
required this.menuUserFg,
|
||||
required this.menuUserHover,
|
||||
required this.toastBg,
|
||||
required this.isDark,
|
||||
});
|
||||
|
||||
@override
|
||||
AppTokens copyWith({
|
||||
Color? page, Color? bg, Color? surface, Color? border, Color? borderSubtle, Color? text,
|
||||
Color? heading, Color? title, Color? muted, Color? faint, Color? primary, Color? primaryDark,
|
||||
Color? onPrimary, Color? brand400, Color? brand50, Color? accent, Color? success, Color? danger,
|
||||
Color? warn, Color? infoSoft, Color? okSoft, Color? accentSoft, Color? successBg, Color? dangerBg,
|
||||
Color? warnBg, Color? thBg, Color? rowHover, Color? shadow, Color? topBg, Color? topFg,
|
||||
Color? topFaint, Color? topBorder, Color? topControl, Color? topMarkBg, Color? topMarkFg,
|
||||
Color? sideBg, Color? sideFg, Color? sideMuted, Color? sideActiveBg, Color? sideActiveFg,
|
||||
Color? sideBorder, Color? sideHover, Color? menuUserBg, Color? menuUserFg, Color? menuUserHover,
|
||||
Color? page,
|
||||
Color? bg,
|
||||
Color? surface,
|
||||
Color? border,
|
||||
Color? borderSubtle,
|
||||
Color? text,
|
||||
Color? heading,
|
||||
Color? title,
|
||||
Color? muted,
|
||||
Color? faint,
|
||||
Color? primary,
|
||||
Color? primaryDark,
|
||||
Color? onPrimary,
|
||||
Color? brand400,
|
||||
Color? brand50,
|
||||
Color? accent,
|
||||
Color? success,
|
||||
Color? danger,
|
||||
Color? warn,
|
||||
Color? infoSoft,
|
||||
Color? okSoft,
|
||||
Color? accentSoft,
|
||||
Color? successBg,
|
||||
Color? dangerBg,
|
||||
Color? warnBg,
|
||||
Color? thBg,
|
||||
Color? rowHover,
|
||||
Color? shadow,
|
||||
Color? topBg,
|
||||
Color? topFg,
|
||||
Color? topFaint,
|
||||
Color? topBorder,
|
||||
Color? topControl,
|
||||
Color? topMarkBg,
|
||||
Color? topMarkFg,
|
||||
Color? sideBg,
|
||||
Color? sideFg,
|
||||
Color? sideMuted,
|
||||
Color? sideActiveBg,
|
||||
Color? sideActiveFg,
|
||||
Color? sideBorder,
|
||||
Color? sideHover,
|
||||
Color? menuUserBg,
|
||||
Color? menuUserFg,
|
||||
Color? menuUserHover,
|
||||
Color? toastBg,
|
||||
bool? isDark,
|
||||
}) {
|
||||
return AppTokens(
|
||||
page: page ?? this.page, bg: bg ?? this.bg, surface: surface ?? this.surface,
|
||||
border: border ?? this.border, borderSubtle: borderSubtle ?? this.borderSubtle,
|
||||
text: text ?? this.text, heading: heading ?? this.heading, title: title ?? this.title,
|
||||
muted: muted ?? this.muted, faint: faint ?? this.faint, primary: primary ?? this.primary,
|
||||
primaryDark: primaryDark ?? this.primaryDark, onPrimary: onPrimary ?? this.onPrimary,
|
||||
brand400: brand400 ?? this.brand400, brand50: brand50 ?? this.brand50, accent: accent ?? this.accent,
|
||||
success: success ?? this.success, danger: danger ?? this.danger, warn: warn ?? this.warn,
|
||||
infoSoft: infoSoft ?? this.infoSoft, okSoft: okSoft ?? this.okSoft, accentSoft: accentSoft ?? this.accentSoft,
|
||||
successBg: successBg ?? this.successBg, dangerBg: dangerBg ?? this.dangerBg, warnBg: warnBg ?? this.warnBg,
|
||||
thBg: thBg ?? this.thBg, rowHover: rowHover ?? this.rowHover, shadow: shadow ?? this.shadow,
|
||||
topBg: topBg ?? this.topBg, topFg: topFg ?? this.topFg, topFaint: topFaint ?? this.topFaint,
|
||||
topBorder: topBorder ?? this.topBorder, topControl: topControl ?? this.topControl,
|
||||
topMarkBg: topMarkBg ?? this.topMarkBg, topMarkFg: topMarkFg ?? this.topMarkFg,
|
||||
sideBg: sideBg ?? this.sideBg, sideFg: sideFg ?? this.sideFg, sideMuted: sideMuted ?? this.sideMuted,
|
||||
sideActiveBg: sideActiveBg ?? this.sideActiveBg, sideActiveFg: sideActiveFg ?? this.sideActiveFg,
|
||||
sideBorder: sideBorder ?? this.sideBorder, sideHover: sideHover ?? this.sideHover,
|
||||
menuUserBg: menuUserBg ?? this.menuUserBg, menuUserFg: menuUserFg ?? this.menuUserFg,
|
||||
menuUserHover: menuUserHover ?? this.menuUserHover, isDark: isDark ?? this.isDark,
|
||||
page: page ?? this.page,
|
||||
bg: bg ?? this.bg,
|
||||
surface: surface ?? this.surface,
|
||||
border: border ?? this.border,
|
||||
borderSubtle: borderSubtle ?? this.borderSubtle,
|
||||
text: text ?? this.text,
|
||||
heading: heading ?? this.heading,
|
||||
title: title ?? this.title,
|
||||
muted: muted ?? this.muted,
|
||||
faint: faint ?? this.faint,
|
||||
primary: primary ?? this.primary,
|
||||
primaryDark: primaryDark ?? this.primaryDark,
|
||||
onPrimary: onPrimary ?? this.onPrimary,
|
||||
brand400: brand400 ?? this.brand400,
|
||||
brand50: brand50 ?? this.brand50,
|
||||
accent: accent ?? this.accent,
|
||||
success: success ?? this.success,
|
||||
danger: danger ?? this.danger,
|
||||
warn: warn ?? this.warn,
|
||||
infoSoft: infoSoft ?? this.infoSoft,
|
||||
okSoft: okSoft ?? this.okSoft,
|
||||
accentSoft: accentSoft ?? this.accentSoft,
|
||||
successBg: successBg ?? this.successBg,
|
||||
dangerBg: dangerBg ?? this.dangerBg,
|
||||
warnBg: warnBg ?? this.warnBg,
|
||||
thBg: thBg ?? this.thBg,
|
||||
rowHover: rowHover ?? this.rowHover,
|
||||
shadow: shadow ?? this.shadow,
|
||||
topBg: topBg ?? this.topBg,
|
||||
topFg: topFg ?? this.topFg,
|
||||
topFaint: topFaint ?? this.topFaint,
|
||||
topBorder: topBorder ?? this.topBorder,
|
||||
topControl: topControl ?? this.topControl,
|
||||
topMarkBg: topMarkBg ?? this.topMarkBg,
|
||||
topMarkFg: topMarkFg ?? this.topMarkFg,
|
||||
sideBg: sideBg ?? this.sideBg,
|
||||
sideFg: sideFg ?? this.sideFg,
|
||||
sideMuted: sideMuted ?? this.sideMuted,
|
||||
sideActiveBg: sideActiveBg ?? this.sideActiveBg,
|
||||
sideActiveFg: sideActiveFg ?? this.sideActiveFg,
|
||||
sideBorder: sideBorder ?? this.sideBorder,
|
||||
sideHover: sideHover ?? this.sideHover,
|
||||
menuUserBg: menuUserBg ?? this.menuUserBg,
|
||||
menuUserFg: menuUserFg ?? this.menuUserFg,
|
||||
menuUserHover: menuUserHover ?? this.menuUserHover,
|
||||
toastBg: toastBg ?? this.toastBg,
|
||||
isDark: isDark ?? this.isDark,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,24 +197,53 @@ class AppTokens extends ThemeExtension<AppTokens> {
|
||||
if (other is! AppTokens) return this;
|
||||
Color c(Color a, Color b) => Color.lerp(a, b, t)!;
|
||||
return AppTokens(
|
||||
page: c(page, other.page), bg: c(bg, other.bg), surface: c(surface, other.surface),
|
||||
border: c(border, other.border), borderSubtle: c(borderSubtle, other.borderSubtle),
|
||||
text: c(text, other.text), heading: c(heading, other.heading), title: c(title, other.title),
|
||||
muted: c(muted, other.muted), faint: c(faint, other.faint), primary: c(primary, other.primary),
|
||||
primaryDark: c(primaryDark, other.primaryDark), onPrimary: c(onPrimary, other.onPrimary),
|
||||
brand400: c(brand400, other.brand400), brand50: c(brand50, other.brand50), accent: c(accent, other.accent),
|
||||
success: c(success, other.success), danger: c(danger, other.danger), warn: c(warn, other.warn),
|
||||
infoSoft: c(infoSoft, other.infoSoft), okSoft: c(okSoft, other.okSoft), accentSoft: c(accentSoft, other.accentSoft),
|
||||
successBg: c(successBg, other.successBg), dangerBg: c(dangerBg, other.dangerBg), warnBg: c(warnBg, other.warnBg),
|
||||
thBg: c(thBg, other.thBg), rowHover: c(rowHover, other.rowHover), shadow: c(shadow, other.shadow),
|
||||
topBg: c(topBg, other.topBg), topFg: c(topFg, other.topFg), topFaint: c(topFaint, other.topFaint),
|
||||
topBorder: c(topBorder, other.topBorder), topControl: c(topControl, other.topControl),
|
||||
topMarkBg: c(topMarkBg, other.topMarkBg), topMarkFg: c(topMarkFg, other.topMarkFg),
|
||||
sideBg: c(sideBg, other.sideBg), sideFg: c(sideFg, other.sideFg), sideMuted: c(sideMuted, other.sideMuted),
|
||||
sideActiveBg: c(sideActiveBg, other.sideActiveBg), sideActiveFg: c(sideActiveFg, other.sideActiveFg),
|
||||
sideBorder: c(sideBorder, other.sideBorder), sideHover: c(sideHover, other.sideHover),
|
||||
menuUserBg: c(menuUserBg, other.menuUserBg), menuUserFg: c(menuUserFg, other.menuUserFg),
|
||||
menuUserHover: c(menuUserHover, other.menuUserHover), isDark: t < 0.5 ? isDark : other.isDark,
|
||||
page: c(page, other.page),
|
||||
bg: c(bg, other.bg),
|
||||
surface: c(surface, other.surface),
|
||||
border: c(border, other.border),
|
||||
borderSubtle: c(borderSubtle, other.borderSubtle),
|
||||
text: c(text, other.text),
|
||||
heading: c(heading, other.heading),
|
||||
title: c(title, other.title),
|
||||
muted: c(muted, other.muted),
|
||||
faint: c(faint, other.faint),
|
||||
primary: c(primary, other.primary),
|
||||
primaryDark: c(primaryDark, other.primaryDark),
|
||||
onPrimary: c(onPrimary, other.onPrimary),
|
||||
brand400: c(brand400, other.brand400),
|
||||
brand50: c(brand50, other.brand50),
|
||||
accent: c(accent, other.accent),
|
||||
success: c(success, other.success),
|
||||
danger: c(danger, other.danger),
|
||||
warn: c(warn, other.warn),
|
||||
infoSoft: c(infoSoft, other.infoSoft),
|
||||
okSoft: c(okSoft, other.okSoft),
|
||||
accentSoft: c(accentSoft, other.accentSoft),
|
||||
successBg: c(successBg, other.successBg),
|
||||
dangerBg: c(dangerBg, other.dangerBg),
|
||||
warnBg: c(warnBg, other.warnBg),
|
||||
thBg: c(thBg, other.thBg),
|
||||
rowHover: c(rowHover, other.rowHover),
|
||||
shadow: c(shadow, other.shadow),
|
||||
topBg: c(topBg, other.topBg),
|
||||
topFg: c(topFg, other.topFg),
|
||||
topFaint: c(topFaint, other.topFaint),
|
||||
topBorder: c(topBorder, other.topBorder),
|
||||
topControl: c(topControl, other.topControl),
|
||||
topMarkBg: c(topMarkBg, other.topMarkBg),
|
||||
topMarkFg: c(topMarkFg, other.topMarkFg),
|
||||
sideBg: c(sideBg, other.sideBg),
|
||||
sideFg: c(sideFg, other.sideFg),
|
||||
sideMuted: c(sideMuted, other.sideMuted),
|
||||
sideActiveBg: c(sideActiveBg, other.sideActiveBg),
|
||||
sideActiveFg: c(sideActiveFg, other.sideActiveFg),
|
||||
sideBorder: c(sideBorder, other.sideBorder),
|
||||
sideHover: c(sideHover, other.sideHover),
|
||||
menuUserBg: c(menuUserBg, other.menuUserBg),
|
||||
menuUserFg: c(menuUserFg, other.menuUserFg),
|
||||
menuUserHover: c(menuUserHover, other.menuUserHover),
|
||||
toastBg: c(toastBg, other.toastBg),
|
||||
isDark: t < 0.5 ? isDark : other.isDark,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ const AppTokens kTokensA = AppTokens(
|
||||
menuUserBg: Color(0xFF3C79C0),
|
||||
menuUserFg: Color(0xFFFFFFFF),
|
||||
menuUserHover: Color(0xFF4D88CC),
|
||||
toastBg: Color(0xFF1F2330),
|
||||
isDark: false,
|
||||
);
|
||||
|
||||
@@ -98,6 +99,7 @@ const AppTokens kTokensB = AppTokens(
|
||||
menuUserBg: Color(0xFF3A3120),
|
||||
menuUserFg: Color(0xFFF0E7D6),
|
||||
menuUserHover: Color(0xFF473B27),
|
||||
toastBg: Color(0xFF1F2330),
|
||||
isDark: true,
|
||||
);
|
||||
|
||||
@@ -147,13 +149,17 @@ const AppTokens kTokensC = AppTokens(
|
||||
menuUserBg: Color(0xFF7E2B3B),
|
||||
menuUserFg: Color(0xFFF7E9D6),
|
||||
menuUserHover: Color(0xFF8E3344),
|
||||
toastBg: Color(0xFF1F2330),
|
||||
isDark: false,
|
||||
);
|
||||
|
||||
AppTokens appTokensOf(String key) {
|
||||
switch (key) {
|
||||
case 'b': return kTokensB;
|
||||
case 'c': return kTokensC;
|
||||
default: return kTokensA;
|
||||
case 'b':
|
||||
return kTokensB;
|
||||
case 'c':
|
||||
return kTokensC;
|
||||
default:
|
||||
return kTokensA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_dims.g.dart';
|
||||
import 'app_tokens.g.dart';
|
||||
|
||||
ThemeData buildTheme(String key) {
|
||||
@@ -18,6 +19,88 @@ ThemeData buildTheme(String key) {
|
||||
brightness: brightness,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: t.bg,
|
||||
// 桌面端默认 adaptivePlatformDensity=compact 会把输入框/按钮压矮 8px,
|
||||
// 与原型 px 规格(.input h38 等)和 golden(测试环境=standard)不一致——
|
||||
// 真机曾因此输入框只有 ~30 高。锁 standard,三端渲染与原型同几何。
|
||||
visualDensity: VisualDensity.standard,
|
||||
// ── 对话框/输入框/按钮的 Material 默认样式钉到 ds 真相源 ──
|
||||
// 原型 .modal:surface / border / r-xl / 标题 fs-h2 heading
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: t.surface,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rXl),
|
||||
side: BorderSide(color: t.border),
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: AppDims.fsH2,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: t.heading),
|
||||
contentTextStyle: TextStyle(fontSize: AppDims.fsBody, color: t.text),
|
||||
),
|
||||
// 原型 .input:h38 盒式(surface/1px border/r-md/pad 0 11),聚焦主色边。
|
||||
// 全局兜底——未显式给 decoration 边框的 TextField/DropdownButtonFormField
|
||||
// 一律盒式,禁止 Material 默认下划线再出现。
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: t.surface,
|
||||
hintStyle: TextStyle(fontSize: AppDims.fsBody, color: t.faint),
|
||||
labelStyle: TextStyle(fontSize: AppDims.fsSm, color: t.muted),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 11, vertical: 9),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
borderSide: BorderSide(color: t.border),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
borderSide: BorderSide(color: t.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
borderSide: BorderSide(color: t.primary),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
borderSide: BorderSide(color: t.border),
|
||||
),
|
||||
),
|
||||
// 原型 .btn.primary:primary 底 / onPrimary 字 / h38 / r-md / w600 / 无阴影
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: t.primary,
|
||||
foregroundColor: t.onPrimary,
|
||||
elevation: 0,
|
||||
minimumSize: const Size(0, 38),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: AppDims.fsBody, fontWeight: FontWeight.w600),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd)),
|
||||
),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: t.primary,
|
||||
foregroundColor: t.onPrimary,
|
||||
minimumSize: const Size(0, 38),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: AppDims.fsBody, fontWeight: FontWeight.w600),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd)),
|
||||
),
|
||||
),
|
||||
// TextButton 兼作行内链接,不加边框;只统一字号字重与圆角
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: const Size(0, 38),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: AppDims.fsBody, fontWeight: FontWeight.w600),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd)),
|
||||
),
|
||||
),
|
||||
// 字体走系统默认(macOS/iOS=PingFang SC,与原型 --font 一致);不强制 Noto。
|
||||
// Noto 仅 golden 测试加载(CI 无中文字体),见 test/support/golden_harness.dart。
|
||||
extensions: <ThemeExtension<dynamic>>[t],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../providers/update_provider.dart';
|
||||
import 'app_updater_io.dart'
|
||||
if (dart.library.js_interop) 'app_updater_web.dart' as impl;
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import 'app_updater_io.dart' if (dart.library.js_interop) 'app_updater_web.dart'
|
||||
as impl;
|
||||
|
||||
/// 应用内更新统一入口。
|
||||
/// - Web:刷新页面。
|
||||
@@ -65,17 +66,11 @@ Future<void> _runWithProgress(
|
||||
title: const Text('更新失败'),
|
||||
content: Text('$e\n\n可改用浏览器下载更新。'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
launchUpdateUrl(info.downloadUrls);
|
||||
},
|
||||
child: const Text('浏览器下载'),
|
||||
),
|
||||
DsButton('取消', onPressed: () => Navigator.pop(ctx)),
|
||||
DsButton('浏览器下载', variant: DsBtnVariant.primary, onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
launchUpdateUrl(info.downloadUrls);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,7 +47,8 @@ Future<void> downloadAndInstall(
|
||||
await Process.run('/bin/rm', ['-rf', extractDir]);
|
||||
await Directory(extractDir).create(recursive: true);
|
||||
// ditto 正确处理 .app 包(保留权限/符号链接)。
|
||||
final unzip = await Process.run('/usr/bin/ditto', ['-x', '-k', zip, extractDir]);
|
||||
final unzip =
|
||||
await Process.run('/usr/bin/ditto', ['-x', '-k', zip, extractDir]);
|
||||
if (unzip.exitCode != 0) {
|
||||
throw Exception('解压失败:${unzip.stderr}');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/// 可注入的应用时钟:业务展示层的「当前时间」统一走 [appNow],
|
||||
/// golden 测试可覆写 [appClock] 冻结时间,保证 相对日期(今天/昨天/剩余天数)确定化。
|
||||
DateTime Function() appClock = () => DateTime.now();
|
||||
|
||||
DateTime appNow() => appClock();
|
||||
@@ -30,7 +30,8 @@ void exportExcel({
|
||||
final blob = web.Blob(
|
||||
<JSAny>[uint8Array].toJS,
|
||||
web.BlobPropertyBag(
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
|
||||
type:
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'),
|
||||
);
|
||||
final url = web.URL.createObjectURL(blob);
|
||||
final anchor = web.document.createElement('a') as web.HTMLAnchorElement
|
||||
|
||||
@@ -6,6 +6,8 @@ import '../errors/error_reporter.dart';
|
||||
import 'label_data.dart';
|
||||
export 'label_data.dart';
|
||||
import 'order_print_meta.dart';
|
||||
import '../theme/context_tokens.dart';
|
||||
import '../../widgets/ds/ds_toast.dart';
|
||||
export 'order_print_meta.dart';
|
||||
import 'print_util_stub.dart'
|
||||
if (dart.library.js_interop) 'print_util_web.dart';
|
||||
@@ -56,7 +58,8 @@ Future<void> printStockOutOrder(StockOutOrder order, OrderPrintMeta meta) =>
|
||||
printStockOutOrderImpl(order, meta);
|
||||
|
||||
/// 生成入库单 PDF 字节(桌面端真实排版,供打印预览光栅化用);Web 端不支持。
|
||||
Future<Uint8List> buildStockInOrderPdf(StockInOrder order, OrderPrintMeta meta) =>
|
||||
Future<Uint8List> buildStockInOrderPdf(
|
||||
StockInOrder order, OrderPrintMeta meta) =>
|
||||
buildStockInOrderPdfImpl(order, meta);
|
||||
|
||||
/// 生成出库单 PDF 字节(桌面端真实排版,供打印预览光栅化用);Web 端不支持。
|
||||
@@ -72,9 +75,7 @@ Future<void> safePrint(BuildContext context, Future<void> Function() fn) async {
|
||||
} catch (e, st) {
|
||||
reportError(e, st);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('打印失败:$e'), backgroundColor: Colors.red),
|
||||
);
|
||||
showDsToast(context, '打印失败:$e', bg: context.tokens.danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ String _fileTs() {
|
||||
}
|
||||
|
||||
// ── 设计色彩 token ──────────────────────────────────────────────────────────
|
||||
const _navy = PdfColor(0.122, 0.165, 0.227); // #1F2A3A header/chip
|
||||
const _cream = PdfColor(0.957, 0.925, 0.847); // #F4ECD8 footer/reversed text
|
||||
const _footnote = PdfColor(0.353, 0.306, 0.208); // #5A4E35 footer text
|
||||
const _ink = PdfColor(0.067, 0.067, 0.067); // #111 body text
|
||||
const _navy = PdfColor(0.122, 0.165, 0.227); // #1F2A3A header/chip
|
||||
const _cream = PdfColor(0.957, 0.925, 0.847); // #F4ECD8 footer/reversed text
|
||||
const _footnote = PdfColor(0.353, 0.306, 0.208); // #5A4E35 footer text
|
||||
const _ink = PdfColor(0.067, 0.067, 0.067); // #111 body text
|
||||
|
||||
// ── 热敏标签 TSPL 裸发(桌面端:dart:ui 画扁平标签位图 → BITMAP → lp -o raw)────────
|
||||
// 得力 DL-888B 等热敏机走系统打印(CUPS 通用驱动)会多走纸/空白,必须发原生 TSPL。
|
||||
@@ -156,8 +156,9 @@ bool _winRawPrint(String printerName, Uint8List data) {
|
||||
}
|
||||
|
||||
/// 在 canvas 上画一行文字(黑色,单行不换行,CJK 走系统字体回退)
|
||||
void _drawText(ui.Canvas canvas, String s, double x, double y, double size,
|
||||
double maxW, {bool bold = false, bool center = false}) {
|
||||
void _drawText(
|
||||
ui.Canvas canvas, String s, double x, double y, double size, double maxW,
|
||||
{bool bold = false, bool center = false}) {
|
||||
if (s.isEmpty) return;
|
||||
final pb = ui.ParagraphBuilder(ui.ParagraphStyle(
|
||||
fontSize: size,
|
||||
@@ -166,7 +167,8 @@ void _drawText(ui.Canvas canvas, String s, double x, double y, double size,
|
||||
maxLines: 1,
|
||||
ellipsis: '',
|
||||
))
|
||||
..pushStyle(ui.TextStyle(color: const ui.Color(0xFF000000)))
|
||||
..pushStyle(
|
||||
ui.TextStyle(color: const ui.Color(0xFF000000))) // ds-ignore: 打印画布纸面固定色
|
||||
..addText(s);
|
||||
final p = pb.build()..layout(ui.ParagraphConstraints(width: maxW));
|
||||
canvas.drawParagraph(p, ui.Offset(x, y));
|
||||
@@ -178,7 +180,8 @@ double _measureWidth(String s, double size, bool bold) {
|
||||
fontSize: size,
|
||||
fontWeight: bold ? ui.FontWeight.bold : ui.FontWeight.normal,
|
||||
maxLines: 1,
|
||||
))..addText(s);
|
||||
))
|
||||
..addText(s);
|
||||
final p = pb.build()..layout(const ui.ParagraphConstraints(width: 100000));
|
||||
return p.maxIntrinsicWidth;
|
||||
}
|
||||
@@ -205,14 +208,14 @@ Future<ui.Image> _renderLabelBitmap({
|
||||
required Uint8List qrBytes,
|
||||
}) async {
|
||||
const w = 320, h = 160; // 40×20mm @203dpi(逻辑尺寸)
|
||||
const scale = 2; // 2× 超采样,内部以 640×320 绘制
|
||||
const scale = 2; // 2× 超采样,内部以 640×320 绘制
|
||||
const sw = w * scale, sh = h * scale;
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(
|
||||
recorder, ui.Rect.fromLTWH(0, 0, sw.toDouble(), sh.toDouble()));
|
||||
final canvas =
|
||||
ui.Canvas(recorder, ui.Rect.fromLTWH(0, 0, sw.toDouble(), sh.toDouble()));
|
||||
canvas.scale(scale.toDouble(), scale.toDouble()); // 坐标系放大,后续坐标不变
|
||||
canvas.drawRect(ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()),
|
||||
ui.Paint()..color = const ui.Color(0xFFFFFFFF));
|
||||
ui.Paint()..color = const ui.Color(0xFFFFFFFF)); // ds-ignore: 打印画布纸面固定色
|
||||
|
||||
// 右侧二维码(解码后端 PNG 直接画上)
|
||||
const qrS = 124.0;
|
||||
@@ -237,27 +240,29 @@ Future<ui.Image> _renderLabelBitmap({
|
||||
const textMaxW = qrLeft - lx - 6; // 164
|
||||
|
||||
// 字号固定:只有商品名按宽度自适应,其余行均固定,杜绝自动放大
|
||||
const double kShopSize = 17.0; // 酒行名,粗体
|
||||
const double kShopSize = 17.0; // 酒行名,粗体
|
||||
const double kSmallSize = 13.0; // 编号 / 度数 / 日期,统一小字
|
||||
// 商品名:仅按宽度适配,上限 26px,下限 14px
|
||||
final double nameSize = _fitFont(name, textMaxW, 1000, true, 26.0).clamp(14.0, 26.0);
|
||||
final double nameSize =
|
||||
_fitFont(name, textMaxW, 1000, true, 26.0).clamp(14.0, 26.0);
|
||||
|
||||
final codeText = code.isNotEmpty ? '编号:$code' : '';
|
||||
final rows = <(String, double, bool)>[
|
||||
if (shop.isNotEmpty) (shop, kShopSize, false), // 店名细体,突出商品名
|
||||
if (name.isNotEmpty) (name, nameSize, true), // 商品名粗体
|
||||
if (shop.isNotEmpty) (shop, kShopSize, false), // 店名细体,突出商品名
|
||||
if (name.isNotEmpty) (name, nameSize, true), // 商品名粗体
|
||||
if (codeText.isNotEmpty) (codeText, kSmallSize, false),
|
||||
if (degSpec.isNotEmpty) (degSpec, kSmallSize, false),
|
||||
if (date.isNotEmpty) (date, kSmallSize, false),
|
||||
if (degSpec.isNotEmpty) (degSpec, kSmallSize, false),
|
||||
if (date.isNotEmpty) (date, kSmallSize, false),
|
||||
];
|
||||
|
||||
// 文字区域:整体略低于二维码顶部,底部延伸至扫码溯源文字下方
|
||||
const double kTextTop = 16.0; // 比二维码顶部(8)低一些,整体下移
|
||||
const double kTextBot = 150.0; // 延伸至底部,减少留白
|
||||
const double kLineH = 1.35; // 行高倍数(字号 × 行高 = 该行占用高度)
|
||||
const double kLineH = 1.35; // 行高倍数(字号 × 行高 = 该行占用高度)
|
||||
final double totalTextH = rows.fold(0.0, (s, r) => s + r.$2 * kLineH);
|
||||
final int n = rows.length;
|
||||
final double rawGap = n > 0 ? (kTextBot - kTextTop - totalTextH) / (n + 1) : 4.0;
|
||||
final double rawGap =
|
||||
n > 0 ? (kTextBot - kTextTop - totalTextH) / (n + 1) : 4.0;
|
||||
final double gap = rawGap < 1.5 ? 1.5 : rawGap;
|
||||
|
||||
double yy = kTextTop + gap;
|
||||
@@ -277,11 +282,13 @@ Uint8List _encodeGbk(String text) {
|
||||
if (Platform.isWindows) {
|
||||
final wideStr = text.toNativeUtf16();
|
||||
try {
|
||||
final needed = WideCharToMultiByte(936, 0, wideStr, -1, nullptr, 0, nullptr, nullptr);
|
||||
final needed = WideCharToMultiByte(
|
||||
936, 0, wideStr, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (needed <= 1) return Uint8List(0);
|
||||
final buf = calloc<Uint8>(needed);
|
||||
try {
|
||||
WideCharToMultiByte(936, 0, wideStr, -1, buf.cast(), needed, nullptr, nullptr);
|
||||
WideCharToMultiByte(
|
||||
936, 0, wideStr, -1, buf.cast(), needed, nullptr, nullptr);
|
||||
return Uint8List.fromList(buf.asTypedList(needed - 1)); // 不含 null 终止符
|
||||
} finally {
|
||||
calloc.free(buf);
|
||||
@@ -337,7 +344,8 @@ Future<bool> _printFlatLabelThermal({
|
||||
const qrWBytes = (qrW + 7) >> 3; // 16 bytes/row
|
||||
|
||||
// 将后端 256×256 QR PNG 缩放到 124×124 并转 1-bit
|
||||
final codec = await ui.instantiateImageCodec(qrBytes, targetWidth: qrW, targetHeight: qrH);
|
||||
final codec = await ui.instantiateImageCodec(qrBytes,
|
||||
targetWidth: qrW, targetHeight: qrH);
|
||||
final qrImg = (await codec.getNextFrame()).image;
|
||||
final qrBd = await qrImg.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
final qrRgba = qrBd!.buffer.asUint8List();
|
||||
@@ -370,24 +378,24 @@ Future<bool> _printFlatLabelThermal({
|
||||
|
||||
// 左侧文字(x=4, 宽度至 x=186)
|
||||
// TSS24.BF2: 24×24 点阵; TSS16.BF2: 16×16 点阵
|
||||
const kFontL = 'TSS16.BF2'; // 小号(编号/度数/日期)
|
||||
const kFontL = 'TSS16.BF2'; // 小号(编号/度数/日期)
|
||||
const kFontXL = 'TSS24.BF2'; // 大号(商品名,≤7字时使用)
|
||||
final nameFont = name.runes.length <= 7 ? kFontXL : kFontL;
|
||||
final nameH = nameFont == kFontXL ? 24 : 16;
|
||||
final nameH = nameFont == kFontXL ? 24 : 16;
|
||||
final codeText = code.isNotEmpty ? '编号:$code' : '';
|
||||
|
||||
// (文本, 字体, 字符高度点数)
|
||||
final rows = <(String, String, int)>[];
|
||||
if (shop.isNotEmpty) rows.add((shop, kFontL, 16));
|
||||
if (name.isNotEmpty) rows.add((name, nameFont, nameH));
|
||||
if (codeText.isNotEmpty) rows.add((codeText, kFontL, 16));
|
||||
if (degSpec.isNotEmpty) rows.add((degSpec, kFontL, 16));
|
||||
if (date.isNotEmpty) rows.add((date, kFontL, 16));
|
||||
if (shop.isNotEmpty) rows.add((shop, kFontL, 16));
|
||||
if (name.isNotEmpty) rows.add((name, nameFont, nameH));
|
||||
if (codeText.isNotEmpty) rows.add((codeText, kFontL, 16));
|
||||
if (degSpec.isNotEmpty) rows.add((degSpec, kFontL, 16));
|
||||
if (date.isNotEmpty) rows.add((date, kFontL, 16));
|
||||
|
||||
// 在 y=8..150 区间均匀分布各行
|
||||
const kTextTop = 8;
|
||||
const kTextBot = 150;
|
||||
const kTextX = 4;
|
||||
const kTextX = 4;
|
||||
final totalH = rows.fold(0, (s, r) => s + r.$3);
|
||||
final n = rows.length;
|
||||
final gapSize = n > 0
|
||||
@@ -423,154 +431,168 @@ Future<void> printProductLabelImpl({
|
||||
String? printerName,
|
||||
}) async {
|
||||
try {
|
||||
final font = await _loadFont();
|
||||
final doc = pw.Document();
|
||||
final qrImage = pw.MemoryImage(qrBytes);
|
||||
final font = await _loadFont();
|
||||
final doc = pw.Document();
|
||||
final qrImage = pw.MemoryImage(qrBytes);
|
||||
|
||||
// Label: 40 × 20 mm(匹配实际标签纸尺寸)
|
||||
const labelW = 40.0 * PdfPageFormat.mm;
|
||||
const labelH = 20.0 * PdfPageFormat.mm;
|
||||
const headerH = labelH * 0.20;
|
||||
const footerH = labelH * 0.11;
|
||||
// Label: 40 × 20 mm(匹配实际标签纸尺寸)
|
||||
const labelW = 40.0 * PdfPageFormat.mm;
|
||||
const labelH = 20.0 * PdfPageFormat.mm;
|
||||
const headerH = labelH * 0.20;
|
||||
const footerH = labelH * 0.11;
|
||||
|
||||
final specVal = (spec ?? '').isNotEmpty ? spec! : '';
|
||||
final seriesVal = (series ?? '').isNotEmpty ? series! : '';
|
||||
// 度数(系列) + 规格 同一行
|
||||
final degSpecVal = [seriesVal, specVal].where((s) => s.isNotEmpty).join(' ');
|
||||
final dateVal = (productionDate ?? '').isNotEmpty
|
||||
? (productionDate!.length > 10 ? productionDate.substring(0, 10) : productionDate)
|
||||
: '—';
|
||||
final specVal = (spec ?? '').isNotEmpty ? spec! : '';
|
||||
final seriesVal = (series ?? '').isNotEmpty ? series! : '';
|
||||
// 度数(系列) + 规格 同一行
|
||||
final degSpecVal =
|
||||
[seriesVal, specVal].where((s) => s.isNotEmpty).join(' ');
|
||||
final dateVal = (productionDate ?? '').isNotEmpty
|
||||
? (productionDate!.length > 10
|
||||
? productionDate.substring(0, 10)
|
||||
: productionDate)
|
||||
: '—';
|
||||
|
||||
// 桌面端热敏机:直接画扁平标签位图 + TSPL 裸发(不走会多走纸的系统打印)
|
||||
if (await _printFlatLabelThermal(
|
||||
shop: shopName,
|
||||
name: name,
|
||||
code: code,
|
||||
degSpec: degSpecVal,
|
||||
date: dateVal == '—' ? '' : dateVal, // 无生产日期(商品详情)则不显示该行
|
||||
qrBytes: qrBytes,
|
||||
printerName: printerName)) {
|
||||
return;
|
||||
}
|
||||
// 桌面端热敏机:直接画扁平标签位图 + TSPL 裸发(不走会多走纸的系统打印)
|
||||
if (await _printFlatLabelThermal(
|
||||
shop: shopName,
|
||||
name: name,
|
||||
code: code,
|
||||
degSpec: degSpecVal,
|
||||
date: dateVal == '—' ? '' : dateVal, // 无生产日期(商品详情)则不显示该行
|
||||
qrBytes: qrBytes,
|
||||
printerName: printerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final contact = [
|
||||
if (shopAddress.isNotEmpty) shopAddress,
|
||||
if (shopPhone.isNotEmpty) shopPhone,
|
||||
].join(' · ');
|
||||
final footerLeft = contact.isNotEmpty ? contact : shopName;
|
||||
final contact = [
|
||||
if (shopAddress.isNotEmpty) shopAddress,
|
||||
if (shopPhone.isNotEmpty) shopPhone,
|
||||
].join(' · ');
|
||||
final footerLeft = contact.isNotEmpty ? contact : shopName;
|
||||
|
||||
// 标签生成时间
|
||||
final now = DateTime.now();
|
||||
final genTime =
|
||||
'${now.year}-${now.month.toString().padLeft(2,'0')}-${now.day.toString().padLeft(2,'0')}'
|
||||
' ${now.hour.toString().padLeft(2,'0')}:${now.minute.toString().padLeft(2,'0')}';
|
||||
// 标签生成时间
|
||||
final now = DateTime.now();
|
||||
final genTime =
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'
|
||||
' ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
doc.addPage(pw.Page(
|
||||
pageFormat: const PdfPageFormat(labelW, labelH),
|
||||
margin: pw.EdgeInsets.zero,
|
||||
build: (_) => pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
doc.addPage(pw.Page(
|
||||
pageFormat: const PdfPageFormat(labelW, labelH),
|
||||
margin: pw.EdgeInsets.zero,
|
||||
build: (_) => pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// ── Header:酒行名称 ─────────────────────────────────────────────────
|
||||
pw.Container(
|
||||
height: headerH,
|
||||
color: _navy,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 4),
|
||||
alignment: pw.Alignment.centerLeft,
|
||||
child: pw.Text(shopName,
|
||||
maxLines: 1,
|
||||
overflow: pw.TextOverflow.clip,
|
||||
style: pw.TextStyle(
|
||||
font: font,
|
||||
fontSize: 6,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: _cream,
|
||||
letterSpacing: 0.5,
|
||||
)),
|
||||
),
|
||||
|
||||
// ── Header:酒行名称 ─────────────────────────────────────────────────
|
||||
pw.Container(
|
||||
height: headerH,
|
||||
color: _navy,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 4),
|
||||
alignment: pw.Alignment.centerLeft,
|
||||
child: pw.Text(shopName,
|
||||
maxLines: 1, overflow: pw.TextOverflow.clip,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 6,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: _cream, letterSpacing: 0.5,
|
||||
)),
|
||||
),
|
||||
|
||||
// ── Body ─────────────────────────────────────────────────────────────
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
color: PdfColors.white,
|
||||
padding: const pw.EdgeInsets.fromLTRB(4, 2, 4, 2),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
// ── Body ─────────────────────────────────────────────────────────────
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
color: PdfColors.white,
|
||||
padding: const pw.EdgeInsets.fromLTRB(4, 2, 4, 2),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 商品名(过长自动缩小并最多两行)
|
||||
pw.Text(name,
|
||||
maxLines: 2,
|
||||
overflow: pw.TextOverflow.clip,
|
||||
style: pw.TextStyle(
|
||||
font: font,
|
||||
fontSize: name.runes.length > 10
|
||||
? 5.5
|
||||
: (name.runes.length > 7 ? 6.5 : 7.5),
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: _ink,
|
||||
letterSpacing: 0.3)),
|
||||
pw.SizedBox(height: 2.5),
|
||||
// 商品编号(度数上方)
|
||||
if (code.isNotEmpty) ...[
|
||||
pw.Text('编号:$code',
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 5.5, color: _ink)),
|
||||
pw.SizedBox(height: 1.5),
|
||||
],
|
||||
// 度数(系列) + 规格 同一行, 无字段名标签
|
||||
pw.Text(degSpecVal,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 5.5, color: _ink)),
|
||||
pw.SizedBox(height: 1.5),
|
||||
// 生产日期(只显示日期,无标签)
|
||||
pw.Text(dateVal,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 5.5, color: _ink)),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 4),
|
||||
// QR
|
||||
pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 商品名(过长自动缩小并最多两行)
|
||||
pw.Text(name,
|
||||
maxLines: 2,
|
||||
overflow: pw.TextOverflow.clip,
|
||||
pw.SizedBox(
|
||||
width: 38,
|
||||
height: 38,
|
||||
child: pw.Image(qrImage, fit: pw.BoxFit.contain),
|
||||
),
|
||||
pw.SizedBox(height: 1),
|
||||
pw.Text('扫码溯源',
|
||||
style: pw.TextStyle(
|
||||
font: font,
|
||||
fontSize: name.runes.length > 10
|
||||
? 5.5
|
||||
: (name.runes.length > 7 ? 6.5 : 7.5),
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: _ink, letterSpacing: 0.3)),
|
||||
pw.SizedBox(height: 2.5),
|
||||
// 商品编号(度数上方)
|
||||
if (code.isNotEmpty) ...[
|
||||
pw.Text('编号:$code',
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _ink)),
|
||||
pw.SizedBox(height: 1.5),
|
||||
],
|
||||
// 度数(系列) + 规格 同一行, 无字段名标签
|
||||
pw.Text(degSpecVal,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _ink)),
|
||||
pw.SizedBox(height: 1.5),
|
||||
// 生产日期(只显示日期,无标签)
|
||||
pw.Text(dateVal,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _ink)),
|
||||
fontSize: 3.5,
|
||||
color: PdfColors.grey600)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Footer ───────────────────────────────────────────────────────────
|
||||
pw.Container(
|
||||
height: footerH,
|
||||
color: _cream,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 4),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Flexible(
|
||||
child: pw.Text(footerLeft,
|
||||
maxLines: 1,
|
||||
overflow: pw.TextOverflow.clip,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 3, color: _footnote)),
|
||||
),
|
||||
pw.SizedBox(width: 4),
|
||||
// QR
|
||||
pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
children: [
|
||||
pw.SizedBox(
|
||||
width: 38, height: 38,
|
||||
child: pw.Image(qrImage, fit: pw.BoxFit.contain),
|
||||
),
|
||||
pw.SizedBox(height: 1),
|
||||
pw.Text('扫码溯源',
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 3.5,
|
||||
color: PdfColors.grey600)),
|
||||
],
|
||||
),
|
||||
pw.Text(genTime,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 3, color: _footnote)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Footer ───────────────────────────────────────────────────────────
|
||||
pw.Container(
|
||||
height: footerH,
|
||||
color: _cream,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 4),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Flexible(
|
||||
child: pw.Text(footerLeft,
|
||||
maxLines: 1, overflow: pw.TextOverflow.clip,
|
||||
style: pw.TextStyle(font: font, fontSize: 3, color: _footnote)),
|
||||
),
|
||||
pw.Text(genTime,
|
||||
style: pw.TextStyle(font: font, fontSize: 3, color: _footnote)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
// 预生成 PDF 字节,避免 macOS NSPrintPanel 打开后再调 doc.save() 导致主线程卡住。
|
||||
final bytes = await doc.save();
|
||||
@@ -593,7 +615,8 @@ Future<void> printProductLabelImpl({
|
||||
Future<Uint8List> renderLabelPreviewImpl(LabelData label) async {
|
||||
final specVal = (label.spec ?? '').isNotEmpty ? label.spec! : '';
|
||||
final seriesVal = (label.series ?? '').isNotEmpty ? label.series! : '';
|
||||
final degSpecVal = [seriesVal, specVal].where((s) => s.isNotEmpty).join(' ');
|
||||
final degSpecVal =
|
||||
[seriesVal, specVal].where((s) => s.isNotEmpty).join(' ');
|
||||
final dateVal = (label.productionDate ?? '').isNotEmpty
|
||||
? (label.productionDate!.length > 10
|
||||
? label.productionDate!.substring(0, 10)
|
||||
@@ -658,15 +681,33 @@ String _qtyStr(double q) =>
|
||||
|
||||
/// 单据明细列(入库/出库统一):商品编号→备注。
|
||||
const _orderHeaders = [
|
||||
'商品编号', '商品名称', '系列', '规格', '批次号',
|
||||
'生产日期', '数量', '单价', '金额', '备注',
|
||||
'商品编号',
|
||||
'商品名称',
|
||||
'系列',
|
||||
'规格',
|
||||
'批次号',
|
||||
'生产日期',
|
||||
'数量',
|
||||
'单价',
|
||||
'金额',
|
||||
'备注',
|
||||
];
|
||||
// 针打版式列宽:日期 / 单价 / 金额 加宽,避免大字号下数字、日期断行。
|
||||
const _orderColFlex = <double>[1.4, 1.8, 1.0, 1.2, 1.6, 1.45, 0.7, 1.45, 1.45, 0.7];
|
||||
const _orderColFlex = <double>[
|
||||
1.4,
|
||||
1.8,
|
||||
1.0,
|
||||
1.2,
|
||||
1.6,
|
||||
1.45,
|
||||
0.7,
|
||||
1.45,
|
||||
1.45,
|
||||
0.7
|
||||
];
|
||||
|
||||
/// 温馨提示文案(入库/出库统一)。
|
||||
const _kTipsText =
|
||||
'温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,'
|
||||
const _kTipsText = '温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,'
|
||||
'如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。';
|
||||
|
||||
/// 各明细列对齐:系列/规格/批次号/生产日期居中,数量/单价/金额右对齐,其余左对齐。
|
||||
@@ -691,20 +732,20 @@ pw.TextAlign _colAlign(int i) {
|
||||
List<pw.Widget> _buildOrderDoc({
|
||||
required pw.Font font,
|
||||
required pw.Font bold,
|
||||
required String shopName, // 公司名称(顶部抬头)
|
||||
required String title, // 入 库 单 / 出 库 单
|
||||
required String shopName, // 公司名称(顶部抬头)
|
||||
required String title, // 入 库 单 / 出 库 单
|
||||
required String orderNo,
|
||||
required String noPrefix, // 'NO:' / 'NO.'
|
||||
required String partnerLabel, // 单位名称 / 客户名称
|
||||
required String noPrefix, // 'NO:' / 'NO.'
|
||||
required String partnerLabel, // 单位名称 / 客户名称
|
||||
required String? partnerName,
|
||||
required String dateLabel, // 入库日期 / 出库日期
|
||||
required String dateLabel, // 入库日期 / 出库日期
|
||||
required String? orderDate,
|
||||
required String addressLabel, // 单位地址 / 送货地址
|
||||
required String addressLabel, // 单位地址 / 送货地址
|
||||
required String? warehouseName,
|
||||
required String signerLabel, // 出货人(签字)/ 收货人(签字)
|
||||
required String signerLabel, // 出货人(签字)/ 收货人(签字)
|
||||
required String operatorRoleLabel, // 采购员 / 销售员(= 经办人)
|
||||
required String? operatorName,
|
||||
required String? makerName, // 制单人 = 打印人
|
||||
required String? makerName, // 制单人 = 打印人
|
||||
required String? remark,
|
||||
required bool showTips,
|
||||
required List<List<String>> rows,
|
||||
@@ -717,13 +758,13 @@ List<pw.Widget> _buildOrderDoc({
|
||||
// 数字等宽(内置 Courier,ASCII 数字适用)。
|
||||
final mono = pw.Font.courierBold();
|
||||
final monoBold = pw.Font.courierBold();
|
||||
final cellStyle = pw.TextStyle(
|
||||
font: bold, fontSize: 9, fontWeight: pw.FontWeight.bold);
|
||||
final cellStyle =
|
||||
pw.TextStyle(font: bold, fontSize: 9, fontWeight: pw.FontWeight.bold);
|
||||
final numStyle = pw.TextStyle(font: mono, fontSize: 9);
|
||||
final headStyle =
|
||||
pw.TextStyle(font: bold, fontSize: 9.5, fontWeight: pw.FontWeight.bold);
|
||||
final metaStyle = pw.TextStyle(
|
||||
font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||
final metaStyle =
|
||||
pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||
final metaBold =
|
||||
pw.TextStyle(font: bold, fontSize: 10.5, fontWeight: pw.FontWeight.bold);
|
||||
|
||||
@@ -801,8 +842,8 @@ List<pw.Widget> _buildOrderDoc({
|
||||
partnerLabel, partnerName ?? '', pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 4,
|
||||
child:
|
||||
metaText(dateLabel, _d10(orderDate), pw.TextAlign.left)),
|
||||
child: metaText(
|
||||
dateLabel, _d10(orderDate), pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 3,
|
||||
child: pw.Text('$noPrefix$orderNo',
|
||||
@@ -813,8 +854,7 @@ List<pw.Widget> _buildOrderDoc({
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
flex: 5,
|
||||
child: metaText('联系电话', '', pw.TextAlign.left)),
|
||||
flex: 5, child: metaText('联系电话', '', pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 4,
|
||||
child: metaText(addressLabel, '', pw.TextAlign.left)),
|
||||
@@ -1021,7 +1061,8 @@ Future<Uint8List> buildStockInOrderPdfImpl(
|
||||
}
|
||||
|
||||
return _buildOrderPdf(() => _buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
font: font,
|
||||
bold: bold,
|
||||
shopName: meta.shopName,
|
||||
title: '入 库 单',
|
||||
orderNo: order.orderNo,
|
||||
@@ -1091,7 +1132,8 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
|
||||
}
|
||||
|
||||
return _buildOrderPdf(() => _buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
font: font,
|
||||
bold: bold,
|
||||
shopName: meta.shopName,
|
||||
title: '出 库 单',
|
||||
orderNo: order.orderNo,
|
||||
|
||||
@@ -33,12 +33,14 @@ Future<void> printProductLabelImpl({
|
||||
}) async {
|
||||
final base64Img = base64Encode(qrBytes);
|
||||
|
||||
final specVal = (spec ?? '').isNotEmpty ? spec! : '';
|
||||
final specVal = (spec ?? '').isNotEmpty ? spec! : '';
|
||||
final seriesVal = (series ?? '').isNotEmpty ? series! : '';
|
||||
// 度数(系列) + 规格 同一行
|
||||
final degSpecVal = [seriesVal, specVal].where((s) => s.isNotEmpty).join(' ');
|
||||
final dateVal = (productionDate ?? '').isNotEmpty
|
||||
? (productionDate!.length > 10 ? productionDate.substring(0, 10) : productionDate)
|
||||
final dateVal = (productionDate ?? '').isNotEmpty
|
||||
? (productionDate!.length > 10
|
||||
? productionDate.substring(0, 10)
|
||||
: productionDate)
|
||||
: '—';
|
||||
// 商品名过长时缩小字号(适当缩小,配合两行折行)
|
||||
final nameLen = name.runes.length;
|
||||
|
||||
Reference in New Issue
Block a user