Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 337a358d54 | |||
| c98a90472b | |||
| 207a9a8343 | |||
| d8c0bd74d2 | |||
| 056ae914ce | |||
| 022443476c | |||
| 93f4f87e44 | |||
| 20e7d07be3 | |||
| 1834740676 | |||
| 90a194fc00 | |||
| fba8b0a917 | |||
| c001f0e8e6 | |||
| f039bd0c61 | |||
| f4beb90769 |
@@ -5,6 +5,44 @@ 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.0.16] - 2026-06-05
|
||||
|
||||
### 新功能
|
||||
- 应用内更新:Windows/macOS 点「立即更新」直接在应用内下载并自动安装/重启(带进度条),规避浏览器 SmartScreen 下载拦截;Web 为刷新页面
|
||||
- Windows 安装程序改为中文向导
|
||||
|
||||
### 改进
|
||||
- 「检查更新」按钮增加反馈提示(正在检查 / 已是最新 / 发现新版本 / 检查失败)
|
||||
- 更新提示文案优化:横幅显示「建议立即更新以获得最新功能与修复」,不再展示冗长更新内容
|
||||
|
||||
## [1.0.15] - 2026-06-05
|
||||
|
||||
### 修复
|
||||
- 修复 Windows 桌面端打印卡死:打印字体由 OpenType/CFF 改为 TrueType(glyf),dart_pdf 渲染中文不再抛错(此前点「打印」后卡死)
|
||||
|
||||
### 改进
|
||||
- 「关于我们」内容改为真实信息(服务商:岩美技术有限公司;网站:jiu.yanmei.com;邮箱:yammy2023@163.com),且统一从配置文件读取,不再硬编码
|
||||
- Windows 桌面端窗口标题由 jiu_client 改为「岩美酒库」
|
||||
|
||||
## [1.0.13] - 2026-06-04
|
||||
|
||||
### 新功能
|
||||
- Windows 客户端改为提供安装程序(Inno Setup 打包的 `jiu-windows-x64-setup.exe`,含安装向导、桌面/开始菜单快捷方式、卸载项),不再是解压版 zip
|
||||
|
||||
### 修复
|
||||
- 修复客户端「无法安全下载」:下载地址从内网 HTTP Forgejo(`http://192.168.3.200:3000`,公网不可达且被浏览器拦截)改为同源 HTTPS `https://jiu.51yanmei.com/downloads/...`,Windows、macOS 均生效
|
||||
- 部署时将最新安装包发布到 web 服务(`/opt/jiu/downloads/`,nginx `/downloads/` 提供),仅保留最新版本(清旧放新)
|
||||
|
||||
## [1.0.9] - 2026-06-04
|
||||
|
||||
### 修复
|
||||
- 适配 nginx 容器化部署:nginx 已迁入 `pangolin-edge` 容器(host 网络),deploy 改为将 jiu.conf 写入 pangolin 挂载目录并 `docker exec pangolin-edge nginx -s reload`,不再操作已废弃的主机 nginx
|
||||
|
||||
## [1.0.8] - 2026-06-03
|
||||
|
||||
### 修复
|
||||
- 修复 EC2 部署最后一步 nginx reload 失败(`invalid PID number`):改用 `systemctl reload-or-restart nginx`(经 systemd MAINPID 发 HUP,不依赖空的 /run/nginx.pid),并 `enable nginx` 确保重启后自启
|
||||
|
||||
## [1.0.7] - 2026-06-03
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"provider": "岩美技术有限公司",
|
||||
"website": "https://jiu.yanmei.com",
|
||||
"email": "yammy2023@163.com"
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 应用「关于我们」等品牌信息,从配置文件 assets/config/app_info.json 读取,
|
||||
/// 不硬编码、不走环境变量。在 main() 启动时调用 [load] 加载一次。
|
||||
class AppInfo {
|
||||
const AppInfo._();
|
||||
|
||||
static const _assetPath = 'assets/config/app_info.json';
|
||||
|
||||
static String provider = '';
|
||||
static String website = '';
|
||||
static String email = '';
|
||||
|
||||
/// 加载配置文件。失败时保留为空(不影响应用启动)。
|
||||
static Future<void> load() async {
|
||||
try {
|
||||
final raw = await rootBundle.loadString(_assetPath);
|
||||
final map = jsonDecode(raw) as Map<String, dynamic>;
|
||||
provider = (map['provider'] as String?) ?? '';
|
||||
website = (map['website'] as String?) ?? '';
|
||||
email = (map['email'] as String?) ?? '';
|
||||
} catch (e) {
|
||||
debugPrint('AppInfo.load failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
|
||||
/// 应用内更新统一入口。
|
||||
/// - Web:刷新页面。
|
||||
/// - Windows/macOS:应用内下载安装包并自动安装/重启(带进度对话框)。
|
||||
/// - 其它平台 / 无对应下载地址:回退浏览器下载(launchUpdateUrl)。
|
||||
Future<void> startInAppUpdate(BuildContext context, AppUpdateInfo info) async {
|
||||
if (impl.isWeb) {
|
||||
impl.reloadForUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
final url = impl.platformDownloadUrl(info.downloadUrls);
|
||||
if (!impl.isInAppSupported || url == null || url.isEmpty) {
|
||||
await launchUpdateUrl(info.downloadUrls); // 兜底:开浏览器/商店
|
||||
return;
|
||||
}
|
||||
|
||||
await _runWithProgress(context, info, url);
|
||||
}
|
||||
|
||||
Future<void> _runWithProgress(
|
||||
BuildContext context, AppUpdateInfo info, String url) async {
|
||||
final progress = ValueNotifier<double>(0);
|
||||
|
||||
// 不可关闭的进度对话框;下载完成后 impl 会让进程退出。
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: Text('正在更新到 v${info.latestVersion}'),
|
||||
content: ValueListenableBuilder<double>(
|
||||
valueListenable: progress,
|
||||
builder: (_, p, __) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LinearProgressIndicator(value: p > 0 ? p : null),
|
||||
const SizedBox(height: 12),
|
||||
Text(p >= 1
|
||||
? '下载完成,正在安装,应用即将重启…'
|
||||
: '正在下载… ${(p * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await impl.downloadAndInstall(url, (p) => progress.value = p);
|
||||
// 桌面端到此通常已 exit(0),不会继续执行。
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context, rootNavigator: true).pop(); // 关进度对话框
|
||||
if (!context.mounted) return;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
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('浏览器下载'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
progress.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
// 桌面/原生平台的应用内更新实现。Web 版见 app_updater_web.dart。
|
||||
|
||||
bool get isWeb => false;
|
||||
|
||||
/// 仅 Windows / macOS 支持「下载并自动安装」。其它平台回退到浏览器下载。
|
||||
bool get isInAppSupported => Platform.isWindows || Platform.isMacOS;
|
||||
|
||||
String? platformDownloadUrl(Map<String, String> urls) {
|
||||
if (Platform.isWindows) return urls['windows'];
|
||||
if (Platform.isMacOS) return urls['macos'];
|
||||
if (Platform.isAndroid) return urls['android'];
|
||||
if (Platform.isIOS) return urls['ios'];
|
||||
return urls['web'];
|
||||
}
|
||||
|
||||
void reloadForUpdate() {/* 桌面无意义 */}
|
||||
|
||||
/// 用 Dio 下载安装包(不经浏览器,无 MOTW / quarantine),再启动安装。
|
||||
/// 成功后进程会自行退出,调用方的后续代码可能不会执行。
|
||||
Future<void> downloadAndInstall(
|
||||
String url, void Function(double) onProgress) async {
|
||||
final tmp = await getTemporaryDirectory();
|
||||
final dio = Dio();
|
||||
void prog(int r, int t) {
|
||||
if (t > 0) onProgress(r / t);
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
final exe = '${tmp.path}\\jiu-update-setup.exe';
|
||||
await dio.download(url, exe, onReceiveProgress: prog);
|
||||
// 启动 Inno Setup 安装包后退出本应用,让其覆盖安装。
|
||||
await Process.start(exe, const [], mode: ProcessStartMode.detached);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
final zip = '${tmp.path}/jiu-update.zip';
|
||||
await dio.download(url, zip, onReceiveProgress: prog);
|
||||
|
||||
final extractDir = '${tmp.path}/jiu-update';
|
||||
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]);
|
||||
if (unzip.exitCode != 0) {
|
||||
throw Exception('解压失败:${unzip.stderr}');
|
||||
}
|
||||
final newApp = Directory(extractDir)
|
||||
.listSync()
|
||||
.map((e) => e.path)
|
||||
.firstWhere((p) => p.endsWith('.app'),
|
||||
orElse: () => throw Exception('更新包中未找到 .app'));
|
||||
|
||||
// 当前运行的 app bundle:.../Xxx.app/Contents/MacOS/exe
|
||||
final resolved = Platform.resolvedExecutable;
|
||||
final idx = resolved.indexOf('.app');
|
||||
if (idx < 0) {
|
||||
throw Exception('无法定位当前 app 路径(非 .app 运行)');
|
||||
}
|
||||
final appPath = resolved.substring(0, idx + 4);
|
||||
|
||||
// 等本进程退出后替换并重启;替换失败(权限)则在访达中高亮让用户手动拖入。
|
||||
final script = '${tmp.path}/jiu-update-swap.sh';
|
||||
await File(script).writeAsString('''#!/bin/sh
|
||||
while kill -0 $pid 2>/dev/null; do sleep 0.5; done
|
||||
if rm -rf "$appPath" && ditto "$newApp" "$appPath"; then
|
||||
open "$appPath"
|
||||
else
|
||||
open -R "$newApp"
|
||||
fi
|
||||
''');
|
||||
await Process.start('/bin/sh', [script], mode: ProcessStartMode.detached);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
throw Exception('当前平台不支持应用内更新');
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
// Web 平台:没有"安装",更新即刷新页面拿最新资源。
|
||||
|
||||
bool get isWeb => true;
|
||||
bool get isInAppSupported => true;
|
||||
|
||||
String? platformDownloadUrl(Map<String, String> urls) => urls['web'];
|
||||
|
||||
void reloadForUpdate() => web.window.location.reload();
|
||||
|
||||
Future<void> downloadAndInstall(
|
||||
String url, void Function(double) onProgress) async {
|
||||
reloadForUpdate();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
import 'core/auth/auth_state.dart';
|
||||
import 'core/config/app_info.dart';
|
||||
import 'core/errors/error_reporter.dart';
|
||||
import 'core/router/app_router.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
@@ -24,7 +25,11 @@ void main() {
|
||||
};
|
||||
|
||||
runZonedGuarded(
|
||||
() => runApp(const ProviderScope(child: JiuApp())),
|
||||
() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息
|
||||
runApp(const ProviderScope(child: JiuApp()));
|
||||
},
|
||||
(error, stack) {
|
||||
debugPrint('═══ Zone Error ═══════════════════════════════');
|
||||
debugPrint(error.toString());
|
||||
|
||||
@@ -11,6 +11,8 @@ import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../core/auth/auth_state.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/config/app_info.dart';
|
||||
import '../../core/update/app_updater.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/number_rule.dart';
|
||||
import '../../models/user.dart';
|
||||
@@ -728,7 +730,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
value: 'v${updateInfo.latestVersion}',
|
||||
valueColor: AppTheme.success,
|
||||
trailing: TextButton(
|
||||
onPressed: () => launchUpdateUrl(updateInfo.downloadUrls),
|
||||
onPressed: () => startInAppUpdate(context, updateInfo),
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
)
|
||||
@@ -738,8 +740,24 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
value: updateInfo != null ? '已是最新' : '检查中…',
|
||||
valueColor: AppTheme.textSecondary,
|
||||
trailing: TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(updateProvider.notifier).forceCheck(),
|
||||
onPressed: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('正在检查更新…')));
|
||||
await ref.read(updateProvider.notifier).forceCheck();
|
||||
if (!context.mounted) return;
|
||||
final r = ref.read(updateProvider).valueOrNull;
|
||||
messenger.hideCurrentSnackBar();
|
||||
final String msg;
|
||||
if (r == null) {
|
||||
msg = '检查更新失败,请检查网络';
|
||||
} else if (r.hasUpdate) {
|
||||
msg = '发现新版本 v${r.latestVersion}';
|
||||
} else {
|
||||
msg = '已是最新版本(v${r.latestVersion})';
|
||||
}
|
||||
messenger.showSnackBar(SnackBar(content: Text(msg)));
|
||||
},
|
||||
child: const Text('检查更新'),
|
||||
),
|
||||
),
|
||||
@@ -828,9 +846,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
_AboutSection(
|
||||
title: '关于我们',
|
||||
children: [
|
||||
const _AboutRow(label: '开发商', value: '酒库科技有限公司'),
|
||||
const _AboutRow(label: '官方网站', value: 'https://jiu.example.com'),
|
||||
const _AboutRow(label: '联系邮箱', value: 'support@jiu.example.com'),
|
||||
_AboutRow(label: '服务商', value: AppInfo.provider),
|
||||
_AboutRow(label: '官方网站', value: AppInfo.website),
|
||||
_AboutRow(label: '联系邮箱', value: AppInfo.email),
|
||||
const _AboutRow(label: '技术支持', value: '周一至周五 9:00 - 18:00'),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
@@ -838,8 +856,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse('mailto:support@jiu.example.com'
|
||||
'?subject=酒库管理系统咨询');
|
||||
final uri = Uri.parse(
|
||||
'mailto:${AppInfo.email}?subject=酒库管理系统咨询');
|
||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||
},
|
||||
icon: const Icon(Icons.email_outlined, size: 16),
|
||||
@@ -887,17 +905,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('续费 / 升级授权'),
|
||||
content: const Column(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('请联系我们获取续费报价:'),
|
||||
SizedBox(height: 12),
|
||||
SelectableText('📧 support@jiu.example.com',
|
||||
style: TextStyle(fontSize: 13)),
|
||||
SizedBox(height: 6),
|
||||
SelectableText('📞 400-000-0000',
|
||||
style: TextStyle(fontSize: 13)),
|
||||
const Text('请联系我们获取续费报价:'),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText('📧 ${AppInfo.email}',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -908,7 +923,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(
|
||||
const ClipboardData(text: 'support@jiu.example.com'));
|
||||
ClipboardData(text: AppInfo.email));
|
||||
if (ctx.mounted) {
|
||||
Navigator.pop(ctx);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -952,7 +967,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
final subject = Uri.encodeComponent(isBug ? 'Bug反馈' : '功能建议');
|
||||
final body = Uri.encodeComponent(ctrl.text);
|
||||
final uri = Uri.parse(
|
||||
'mailto:support@jiu.example.com?subject=$subject&body=$body');
|
||||
'mailto:${AppInfo.email}?subject=$subject&body=$body');
|
||||
if (await canLaunchUrl(uri)) launchUrl(uri);
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../core/theme/app_theme.dart';
|
||||
import '../../providers/connectivity_provider.dart';
|
||||
import '../../providers/shop_provider.dart';
|
||||
import '../../providers/update_provider.dart';
|
||||
import '../../core/update/app_updater.dart';
|
||||
|
||||
class AppShell extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
@@ -41,22 +42,11 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
Text('发现新版本'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('当前版本需要更新至 v${info.latestVersion} 才能继续使用。'),
|
||||
if (info.releaseNotes.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(info.releaseNotes,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary, fontSize: 13)),
|
||||
],
|
||||
],
|
||||
),
|
||||
content: Text('发现新版本 v${info.latestVersion},需更新后才能继续使用,'
|
||||
'更新以获得最新功能与修复。'),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => launchUpdateUrl(info.downloadUrls),
|
||||
onPressed: () => startInAppUpdate(context, info),
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
],
|
||||
@@ -228,8 +218,8 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'发现新版本 v${updateInfo.latestVersion}'
|
||||
'${updateInfo.releaseNotes.isNotEmpty ? " · ${updateInfo.releaseNotes}" : ""}',
|
||||
'发现新版本 v${updateInfo.latestVersion},'
|
||||
'建议立即更新以获得最新功能与修复',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF5D4037),
|
||||
fontSize: 13),
|
||||
@@ -237,8 +227,8 @@ class _AppShellState extends ConsumerState<AppShell> {
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => launchUpdateUrl(
|
||||
updateInfo.downloadUrls),
|
||||
onPressed: () =>
|
||||
startInAppUpdate(context, updateInfo),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
const Color(0xFFF57F17)),
|
||||
|
||||
@@ -37,3 +37,4 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/fonts/NotoSansSC-Regular.ttf
|
||||
- assets/config/app_info.json
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
// 回归测试:打印用的中文字体必须是 TrueType(glyf) 轮廓。
|
||||
// 若误用 OpenType/CFF 字体(如 Noto 的 .otf),dart_pdf 渲染中文会抛
|
||||
// "This font does not support Unicode characters",标签打印会在原生 onLayout
|
||||
// 回调里抛异常导致 Windows 打印卡死。此测试确保打印 PDF 能正常生成中文。
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('打印字体能渲染中文(防止退回 CFF 字体导致打印卡死)', () async {
|
||||
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||
final font = pw.Font.ttf(data);
|
||||
|
||||
final doc = pw.Document();
|
||||
doc.addPage(pw.Page(
|
||||
build: (_) => pw.Text('入库单 茅台飞天 53度 500ml 扫码溯源',
|
||||
style: pw.TextStyle(font: font, fontSize: 12)),
|
||||
));
|
||||
|
||||
// 若字体是 CFF,会在此抛 Latin1/Unicode 异常。
|
||||
final bytes = await doc.save();
|
||||
expect(bytes.isNotEmpty, true);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
@@ -27,7 +29,19 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"jiu_client", origin, size)) {
|
||||
// Window title as UTF-8 bytes (hex-escaped to keep this source pure ASCII and
|
||||
// avoid MSVC C4819 under GBK code page 936). Decoded to UTF-16 for Win32.
|
||||
// Bytes below are the UTF-8 encoding of the Chinese app title.
|
||||
const std::string title_utf8 =
|
||||
"\xE5\xB2\xA9\xE7\xBE\x8E\xE9\x85\x92\xE5\xBA\x93";
|
||||
int title_len = ::MultiByteToWideChar(CP_UTF8, 0, title_utf8.c_str(), -1,
|
||||
nullptr, 0);
|
||||
std::wstring window_title(title_len > 0 ? title_len - 1 : 0, L'\0');
|
||||
if (title_len > 0) {
|
||||
::MultiByteToWideChar(CP_UTF8, 0, title_utf8.c_str(), -1, &window_title[0],
|
||||
title_len);
|
||||
}
|
||||
if (!window.Create(window_title, origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
@@ -59,6 +59,14 @@ server {
|
||||
try_files /scan.html =404;
|
||||
}
|
||||
|
||||
# 桌面客户端安装包下载(Windows .exe / macOS .zip)
|
||||
location /downloads/ {
|
||||
alias /opt/jiu/downloads/;
|
||||
add_header Content-Disposition "attachment";
|
||||
add_header Cache-Control "no-cache";
|
||||
autoindex off;
|
||||
}
|
||||
|
||||
# 营销站点兜底(根路径及所有其他路径)
|
||||
location / {
|
||||
root /opt/jiu/marketing;
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***
|
||||
;
|
||||
; To download user-contributed translations of this file, go to:
|
||||
; https://jrsoftware.org/files/istrans/
|
||||
;
|
||||
; Note: When translating this text, do not add periods (.) to the end of
|
||||
; messages that didn't have them already, because on those messages Inno
|
||||
; Setup adds the periods automatically (appending a period would result in
|
||||
; two periods being displayed).
|
||||
;
|
||||
; Maintained by Zhenghan Yang
|
||||
; Email: 847320916@QQ.com
|
||||
; Translation based on network resource
|
||||
; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||
;
|
||||
|
||||
[LangOptions]
|
||||
; The following three entries are very important. Be sure to read and
|
||||
; understand the '[LangOptions] section' topic in the help file.
|
||||
LanguageName=简体中文
|
||||
; If Language Name display incorrect, uncomment next line
|
||||
; LanguageName=<7B80><4F53><4E2D><6587>
|
||||
; About LanguageID, to reference link:
|
||||
; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
|
||||
LanguageID=$0804
|
||||
; About CodePage, to reference link:
|
||||
; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
||||
LanguageCodePage=936
|
||||
; If the language you are translating to requires special font faces or
|
||||
; sizes, uncomment any of the following entries and change them accordingly.
|
||||
;DialogFontName=
|
||||
;DialogFontSize=9
|
||||
;DialogFontBaseScaleWidth=7
|
||||
;DialogFontBaseScaleHeight=15
|
||||
;WelcomeFontName=Segoe UI
|
||||
;WelcomeFontSize=14
|
||||
|
||||
[Messages]
|
||||
|
||||
; *** 应用程序标题
|
||||
SetupAppTitle=安装
|
||||
SetupWindowTitle=安装 - %1
|
||||
UninstallAppTitle=卸载
|
||||
UninstallAppFullTitle=%1 卸载
|
||||
|
||||
; *** Misc. common
|
||||
InformationTitle=信息
|
||||
ConfirmTitle=确认
|
||||
ErrorTitle=错误
|
||||
|
||||
; *** SetupLdr messages
|
||||
SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
|
||||
LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
|
||||
LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
|
||||
HelpTextNote=
|
||||
|
||||
; *** 启动错误消息
|
||||
LastErrorMessage=%1。%n%n错误 %2: %3
|
||||
SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
|
||||
SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
|
||||
SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
|
||||
InvalidParameter=无效的命令行参数:%n%n%1
|
||||
SetupAlreadyRunning=安装程序正在运行。
|
||||
WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
|
||||
WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
|
||||
NotOnThisPlatform=此程序不能在 %1 上运行。
|
||||
OnlyOnThisPlatform=此程序只能在 %1 上运行。
|
||||
OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
|
||||
WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
|
||||
WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
|
||||
AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
|
||||
PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。
|
||||
SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||
UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||
|
||||
; *** 启动问题
|
||||
PrivilegesRequiredOverrideTitle=选择安装程序模式
|
||||
PrivilegesRequiredOverrideInstruction=选择安装模式
|
||||
PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
|
||||
PrivilegesRequiredOverrideText2=%1 可以仅为您安装,或为所有用户安装(需要管理员权限)。
|
||||
PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
|
||||
PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)
|
||||
PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)
|
||||
PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项)
|
||||
|
||||
; *** 其他错误
|
||||
ErrorCreatingDir=安装程序无法创建目录“%1”
|
||||
ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
|
||||
|
||||
; *** 安装程序公共消息
|
||||
ExitSetupTitle=退出安装程序
|
||||
ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
|
||||
AboutSetupMenuItem=关于安装程序(&A)...
|
||||
AboutSetupTitle=关于安装程序
|
||||
AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
|
||||
AboutSetupNote=
|
||||
TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||
|
||||
; *** 按钮
|
||||
ButtonBack=< 上一步(&B)
|
||||
ButtonNext=下一步(&N) >
|
||||
ButtonInstall=安装(&I)
|
||||
ButtonOK=确定
|
||||
ButtonCancel=取消
|
||||
ButtonYes=是(&Y)
|
||||
ButtonYesToAll=全是(&A)
|
||||
ButtonNo=否(&N)
|
||||
ButtonNoToAll=全否(&O)
|
||||
ButtonFinish=完成(&F)
|
||||
ButtonBrowse=浏览(&B)...
|
||||
ButtonWizardBrowse=浏览(&R)...
|
||||
ButtonNewFolder=新建文件夹(&M)
|
||||
|
||||
; *** “选择语言”对话框消息
|
||||
SelectLanguageTitle=选择安装语言
|
||||
SelectLanguageLabel=选择安装时使用的语言。
|
||||
|
||||
; *** 公共向导文字
|
||||
ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
|
||||
BeveledLabel=
|
||||
BrowseDialogTitle=浏览文件夹
|
||||
BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
|
||||
NewFolderName=新建文件夹
|
||||
|
||||
; *** “欢迎”向导页
|
||||
WelcomeLabel1=欢迎使用 [name] 安装向导
|
||||
WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。
|
||||
|
||||
; *** “密码”向导页
|
||||
WizardPassword=密码
|
||||
PasswordLabel1=这个安装程序有密码保护。
|
||||
PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
|
||||
PasswordEditLabel=密码(&P):
|
||||
IncorrectPassword=您输入的密码不正确,请重新输入。
|
||||
|
||||
; *** “许可协议”向导页
|
||||
WizardLicense=许可协议
|
||||
LicenseLabel=请在继续安装前阅读以下重要信息。
|
||||
LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
|
||||
LicenseAccepted=我同意此协议(&A)
|
||||
LicenseNotAccepted=我不同意此协议(&D)
|
||||
|
||||
; *** “信息”向导页
|
||||
WizardInfoBefore=信息
|
||||
InfoBeforeLabel=请在继续安装前阅读以下重要信息。
|
||||
InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
|
||||
WizardInfoAfter=信息
|
||||
InfoAfterLabel=请在继续安装前阅读以下重要信息。
|
||||
InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
|
||||
|
||||
; *** “用户信息”向导页
|
||||
WizardUserInfo=用户信息
|
||||
UserInfoDesc=请输入您的信息。
|
||||
UserInfoName=用户名(&U):
|
||||
UserInfoOrg=组织(&O):
|
||||
UserInfoSerial=序列号(&S):
|
||||
UserInfoNameRequired=您必须输入用户名。
|
||||
|
||||
; *** “选择目标目录”向导页
|
||||
WizardSelectDir=选择目标位置
|
||||
SelectDirDesc=您想将 [name] 安装在哪里?
|
||||
SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
|
||||
SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||
DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
|
||||
DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
|
||||
CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
|
||||
CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
|
||||
InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share
|
||||
InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
|
||||
DiskSpaceWarningTitle=磁盘空间不足
|
||||
DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
|
||||
DirNameTooLong=文件夹名称或路径太长。
|
||||
InvalidDirName=文件夹名称无效。
|
||||
BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
|
||||
DirExistsTitle=文件夹已存在
|
||||
DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
|
||||
DirDoesntExistTitle=文件夹不存在
|
||||
DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
|
||||
|
||||
; *** “选择组件”向导页
|
||||
WizardSelectComponents=选择组件
|
||||
SelectComponentsDesc=您想安装哪些程序组件?
|
||||
SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
|
||||
FullInstallation=完全安装
|
||||
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
|
||||
CompactInstallation=简洁安装
|
||||
CustomInstallation=自定义安装
|
||||
NoUninstallWarningTitle=组件已存在
|
||||
NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
|
||||
ComponentSize1=%1 KB
|
||||
ComponentSize2=%1 MB
|
||||
ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
|
||||
ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
|
||||
|
||||
; *** “选择附加任务”向导页
|
||||
WizardSelectTasks=选择附加任务
|
||||
SelectTasksDesc=您想要安装程序执行哪些附加任务?
|
||||
SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
|
||||
|
||||
; *** “选择开始菜单文件夹”向导页
|
||||
WizardSelectProgramGroup=选择开始菜单文件夹
|
||||
SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
|
||||
SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
|
||||
SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||
MustEnterGroupName=您必须输入一个文件夹名。
|
||||
GroupNameTooLong=文件夹名或路径太长。
|
||||
InvalidGroupName=无效的文件夹名字。
|
||||
BadGroupName=文件夹名不能包含下列任何字符:%n%n%1
|
||||
NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
|
||||
|
||||
; *** “准备安装”向导页
|
||||
WizardReady=准备安装
|
||||
ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。
|
||||
ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
|
||||
ReadyLabel2b=点击“安装”继续此安装程序。
|
||||
ReadyMemoUserInfo=用户信息:
|
||||
ReadyMemoDir=目标位置:
|
||||
ReadyMemoType=安装类型:
|
||||
ReadyMemoComponents=已选择组件:
|
||||
ReadyMemoGroup=开始菜单文件夹:
|
||||
ReadyMemoTasks=附加任务:
|
||||
|
||||
; *** TExtractionWizardPage 向导页面与 ExtractArchive
|
||||
ExtractingLabel=正在解压文件...
|
||||
ButtonStopExtraction=停止解压(&S)
|
||||
StopExtraction=您确定要停止解压吗?
|
||||
ErrorExtractionAborted=解压已中止
|
||||
ErrorExtractionFailed=解压失败:%1
|
||||
|
||||
; *** 压缩文件解压失败详情
|
||||
ArchiveIncorrectPassword=压缩文件密码不正确
|
||||
ArchiveIsCorrupted=压缩文件已损坏
|
||||
ArchiveUnsupportedFormat=不支持的压缩文件格式
|
||||
|
||||
; *** TDownloadWizardPage 向导页面和 DownloadTemporaryFile
|
||||
DownloadingLabel2=正在下载文件...
|
||||
ButtonStopDownload=停止下载(&S)
|
||||
StopDownload=您确定要停止下载吗?
|
||||
ErrorDownloadAborted=下载已中止
|
||||
ErrorDownloadFailed=下载失败:%1 %2
|
||||
ErrorDownloadSizeFailed=获取下载大小失败:%1 %2
|
||||
ErrorProgress=无效的进度:%1 / %2
|
||||
ErrorFileSize=文件大小错误:预期 %1,实际 %2
|
||||
|
||||
; *** “正在准备安装”向导页
|
||||
WizardPreparing=正在准备安装
|
||||
PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。
|
||||
PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。
|
||||
CannotContinue=安装程序不能继续。请点击“取消”退出。
|
||||
ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
|
||||
ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
|
||||
CloseApplications=自动关闭应用程序(&A)
|
||||
DontCloseApplications=不要关闭应用程序(&D)
|
||||
ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
|
||||
PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动?
|
||||
|
||||
; *** “正在安装”向导页
|
||||
WizardInstalling=正在安装
|
||||
InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。
|
||||
|
||||
; *** “安装完成”向导页
|
||||
FinishedHeadingLabel=[name] 安装完成
|
||||
FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。
|
||||
FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
|
||||
ClickFinish=点击“完成”退出安装程序。
|
||||
FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗?
|
||||
FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗?
|
||||
ShowReadmeCheck=是,我想查阅自述文件
|
||||
YesRadio=是,立即重启电脑(&Y)
|
||||
NoRadio=否,稍后重启电脑(&N)
|
||||
; used for example as 'Run MyProg.exe'
|
||||
RunEntryExec=运行 %1
|
||||
; used for example as 'View Readme.txt'
|
||||
RunEntryShellExec=查阅 %1
|
||||
|
||||
; *** “安装程序需要下一张磁盘”提示
|
||||
ChangeDiskTitle=安装程序需要下一张磁盘
|
||||
SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
|
||||
PathLabel=路径(&P):
|
||||
FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
|
||||
SelectDirectoryLabel=请指定下一张磁盘的位置。
|
||||
|
||||
; *** 安装阶段消息
|
||||
SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
|
||||
AbortRetryIgnoreSelectAction=选择操作
|
||||
AbortRetryIgnoreRetry=重试(&T)
|
||||
AbortRetryIgnoreIgnore=忽略错误并继续(&I)
|
||||
AbortRetryIgnoreCancel=关闭安装程序
|
||||
RetryCancelSelectAction=选择操作
|
||||
RetryCancelRetry=重试(&T)
|
||||
RetryCancelCancel=取消(&C)
|
||||
|
||||
; *** 安装状态消息
|
||||
StatusClosingApplications=正在关闭应用程序...
|
||||
StatusCreateDirs=正在创建目录...
|
||||
StatusExtractFiles=正在提取文件...
|
||||
StatusDownloadFiles=正在下载文件...
|
||||
StatusCreateIcons=正在创建快捷方式...
|
||||
StatusCreateIniEntries=正在创建 INI 条目...
|
||||
StatusCreateRegistryEntries=正在创建注册表条目...
|
||||
StatusRegisterFiles=正在注册文件...
|
||||
StatusSavingUninstall=正在保存卸载信息...
|
||||
StatusRunProgram=正在完成安装...
|
||||
StatusRestartingApplications=正在重启应用程序...
|
||||
StatusRollback=正在撤销更改...
|
||||
|
||||
; *** 其他错误
|
||||
ErrorInternal2=内部错误:%1
|
||||
ErrorFunctionFailedNoCode=%1 失败
|
||||
ErrorFunctionFailed=%1 失败;错误代码 %2
|
||||
ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
|
||||
ErrorExecutingProgram=无法执行文件:%n%1
|
||||
|
||||
; *** 注册表错误
|
||||
ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
|
||||
ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
|
||||
ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
|
||||
|
||||
; *** INI 错误
|
||||
ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
|
||||
|
||||
; *** 文件复制错误
|
||||
FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)
|
||||
FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)
|
||||
SourceIsCorrupted=源文件已损坏
|
||||
SourceDoesntExist=源文件“%1”不存在
|
||||
SourceVerificationFailed=源文件验证失败: %1
|
||||
VerificationSignatureDoesntExist=签名文件“%1”不存在
|
||||
VerificationSignatureInvalid=签名文件“%1”无效
|
||||
VerificationKeyNotFound=签名文件“%1”使用了未知密钥
|
||||
VerificationFileNameIncorrect=文件名不正确
|
||||
VerificationFileTagIncorrect=文件标签不正确
|
||||
VerificationFileSizeIncorrect=文件大小不正确
|
||||
VerificationFileHashIncorrect=文件哈希值不正确
|
||||
ExistingFileReadOnly2=无法替换现有文件,它是只读的。
|
||||
ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
|
||||
ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
|
||||
ErrorReadingExistingDest=尝试读取现有文件时出错:
|
||||
FileExistsSelectAction=选择操作
|
||||
FileExists2=文件已经存在。
|
||||
FileExistsOverwriteExisting=覆盖已存在的文件(&O)
|
||||
FileExistsKeepExisting=保留现有的文件(&K)
|
||||
FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||
ExistingFileNewerSelectAction=选择操作
|
||||
ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
|
||||
ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
|
||||
ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)
|
||||
ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||
ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
|
||||
ErrorCreatingTemp=尝试在目标目录创建文件时出错:
|
||||
ErrorReadingSource=尝试读取下列源文件时出错:
|
||||
ErrorCopying=尝试复制下列文件时出错:
|
||||
ErrorDownloading=下载文件时出错:
|
||||
ErrorExtracting=解压压缩文件时出错:
|
||||
ErrorReplacingExistingFile=尝试替换现有文件时出错:
|
||||
ErrorRestartReplace=重启并替换失败:
|
||||
ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
|
||||
ErrorRegisterServer=无法注册 DLL/OCX:%1
|
||||
ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
|
||||
ErrorRegisterTypeLib=无法注册类库:%1
|
||||
|
||||
; *** 卸载显示名字标记
|
||||
; used for example as 'My Program (32-bit)'
|
||||
UninstallDisplayNameMark=%1 (%2)
|
||||
; used for example as 'My Program (32-bit, All users)'
|
||||
UninstallDisplayNameMarks=%1 (%2, %3)
|
||||
UninstallDisplayNameMark32Bit=32 位
|
||||
UninstallDisplayNameMark64Bit=64 位
|
||||
UninstallDisplayNameMarkAllUsers=所有用户
|
||||
UninstallDisplayNameMarkCurrentUser=当前用户
|
||||
|
||||
; *** 安装后错误
|
||||
ErrorOpeningReadme=尝试打开自述文件时出错。
|
||||
ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。
|
||||
|
||||
; *** 卸载消息
|
||||
UninstallNotFound=文件“%1”不存在。无法卸载。
|
||||
UninstallOpenError=文件“%1”不能被打开。无法卸载。
|
||||
UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
|
||||
UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)
|
||||
ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
|
||||
UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
|
||||
OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
|
||||
UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。
|
||||
UninstalledAll=已顺利从您的电脑中移除 %1。
|
||||
UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
|
||||
UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗?
|
||||
UninstallDataCorrupted=文件“%1”已损坏。无法卸载
|
||||
|
||||
; *** 卸载状态消息
|
||||
ConfirmDeleteSharedFileTitle=删除共享的文件吗?
|
||||
ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。
|
||||
SharedFileNameLabel=文件名:
|
||||
SharedFileLocationLabel=位置:
|
||||
WizardUninstalling=卸载状态
|
||||
StatusUninstalling=正在卸载 %1...
|
||||
|
||||
; *** Shutdown block reasons
|
||||
ShutdownBlockReasonInstallingApp=正在安装 %1。
|
||||
ShutdownBlockReasonUninstallingApp=正在卸载 %1。
|
||||
|
||||
; The custom messages below aren't used by Setup itself, but if you make
|
||||
; use of them in your scripts, you'll want to translate them.
|
||||
|
||||
[CustomMessages]
|
||||
|
||||
NameAndVersion=%1 版本 %2
|
||||
AdditionalIcons=附加快捷方式:
|
||||
CreateDesktopIcon=创建桌面快捷方式(&D)
|
||||
CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
|
||||
ProgramOnTheWeb=%1 网站
|
||||
UninstallProgram=卸载 %1
|
||||
LaunchProgram=运行 %1
|
||||
AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
|
||||
AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
|
||||
AutoStartProgramGroupDescription=启动:
|
||||
AutoStartProgram=自动启动 %1
|
||||
AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
|
||||
@@ -0,0 +1,59 @@
|
||||
; Inno Setup script for 岩美酒库 Windows 客户端
|
||||
; Compiled in CI:
|
||||
; ISCC /DAppVer=<ver> "/DSrcDir=<ReleaseDir>" "/DOutDir=<dist>" jiu-installer.iss
|
||||
; Produces: <OutDir>\jiu-windows-x64-setup.exe
|
||||
|
||||
#ifndef AppVer
|
||||
#define AppVer "0.0.0"
|
||||
#endif
|
||||
#ifndef SrcDir
|
||||
#define SrcDir "."
|
||||
#endif
|
||||
#ifndef OutDir
|
||||
#define OutDir "."
|
||||
#endif
|
||||
|
||||
#define MyAppName "岩美酒库"
|
||||
#define MyAppExe "jiu_client.exe"
|
||||
#define MyAppPublisher "岩美"
|
||||
|
||||
[Setup]
|
||||
AppId={{A3F5C1E2-9B47-4D8A-B6E0-2C1D4F8A9E33}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#AppVer}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
DefaultDirName={autopf}\Jiu
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputDir={#OutDir}
|
||||
OutputBaseFilename=jiu-windows-x64-setup
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
UninstallDisplayName={#MyAppName}
|
||||
UninstallDisplayIcon={app}\{#MyAppExe}
|
||||
; 应用内更新时本应用会先退出再启动安装包;这些选项确保能顺利覆盖。
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
|
||||
[Languages]
|
||||
; 简体中文语言文件随仓库提供(deploy/windows/ChineseSimplified.isl),
|
||||
; compile-windows.sh 会把它和本 .iss 一起拷到构建目录,按相对路径引用,
|
||||
; 不依赖 Inno Setup 安装目录是否自带(默认不带简中)。
|
||||
Name: "chinesesimp"; MessagesFile: "ChineseSimplified.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
|
||||
[Files]
|
||||
Source: "{#SrcDir}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExe}"
|
||||
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExe}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExe}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
|
||||
@@ -44,13 +44,39 @@ flutter build windows --release \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
popd > /dev/null
|
||||
|
||||
# Package the Release folder into dist/ using PowerShell Compress-Archive.
|
||||
# Source is an absolute Windows path; destination is relative to the workspace.
|
||||
# Package the Release folder into a Windows installer with Inno Setup (ISCC).
|
||||
# Inno Setup is installed by provision-windows.sh; check both Program Files dirs.
|
||||
ISCC=""
|
||||
for c in "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" "/c/Program Files/Inno Setup 6/ISCC.exe"; do
|
||||
if [ -f "$c" ]; then ISCC="$c"; break; fi
|
||||
done
|
||||
if [ -z "$ISCC" ]; then
|
||||
echo "ERROR: Inno Setup (ISCC) not found. Run provision-windows.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run ISCC entirely under the clean build root: 32-bit ISCC would hit WOW64
|
||||
# redirection if it read the .iss / wrote output under the System32 workspace.
|
||||
cp deploy/windows/jiu-installer.iss "${BUILD_ROOT}/jiu-installer.iss"
|
||||
# 简体中文语言文件随 .iss 一起放到构建目录,供 [Languages] 按相对路径引用。
|
||||
cp deploy/windows/ChineseSimplified.isl "${BUILD_ROOT}/ChineseSimplified.isl"
|
||||
mkdir -p "${BUILD_ROOT}/dist"
|
||||
echo "==> building installer with Inno Setup (version ${VER})"
|
||||
# MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL disable Git Bash's automatic conversion
|
||||
# of /-leading args into Windows paths, which otherwise mangles ISCC's /D defines
|
||||
# (turning them into extra "script filenames").
|
||||
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "$ISCC" \
|
||||
"/DAppVer=${VER}" \
|
||||
"/DSrcDir=C:\\jiu-build\\client\\build\\windows\\x64\\runner\\Release" \
|
||||
"/DOutDir=C:\\jiu-build\\dist" \
|
||||
"C:\\jiu-build\\jiu-installer.iss"
|
||||
|
||||
# Copy the installer back to the workspace dist/ via PowerShell (64-bit, so it
|
||||
# can write the System32 workspace path without WOW64 redirection).
|
||||
mkdir -p dist
|
||||
REL_WIN='C:\jiu-build\client\build\windows\x64\runner\Release'
|
||||
echo "==> packaging ${REL_WIN} -> dist/jiu-windows-x64.zip"
|
||||
echo "==> copying installer -> dist/jiu-windows-x64-setup.exe"
|
||||
powershell -NoProfile -Command \
|
||||
"Compress-Archive -Path '${REL_WIN}\*' -DestinationPath 'dist\jiu-windows-x64.zip' -Force"
|
||||
"Copy-Item 'C:\\jiu-build\\dist\\jiu-windows-x64-setup.exe' 'dist\\jiu-windows-x64-setup.exe' -Force"
|
||||
|
||||
echo "==> compile-windows: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
|
||||
+18
-3
@@ -90,6 +90,11 @@ rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
/tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/"
|
||||
|
||||
# Upload desktop client installers (served from /downloads/ by nginx).
|
||||
# Guarded: may be absent in a partial manual deploy.
|
||||
[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true
|
||||
[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true
|
||||
|
||||
echo "==> deploy: running remote deploy"
|
||||
|
||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||
@@ -126,9 +131,19 @@ mkdir -p /opt/jiu/marketing
|
||||
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
|
||||
# Update nginx config and reload
|
||||
sudo cp /tmp/nginx-jiu.conf /etc/nginx/conf.d/jiu.conf
|
||||
sudo nginx -t && sudo nginx -s reload
|
||||
# Publish desktop client installers to the nginx-served downloads dir.
|
||||
# Keep only the latest version: clear old installers, then drop in the new ones.
|
||||
mkdir -p /opt/jiu/downloads
|
||||
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* 2>/dev/null || true
|
||||
[ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true
|
||||
|
||||
# Update jiu nginx config in the pangolin-edge reverse proxy.
|
||||
# nginx now runs inside the `pangolin-edge` container (host networking), not on
|
||||
# the host. Its /etc/nginx/conf.d is bind-mounted from the host dir below, so we
|
||||
# write the config there and reload nginx *inside the container*.
|
||||
cp /tmp/nginx-jiu.conf /home/ec2-user/pangolin/edge/conf.d/jiu.conf
|
||||
docker exec pangolin-edge nginx -t && docker exec pangolin-edge nginx -s reload
|
||||
|
||||
echo "Deploy complete!"
|
||||
ENDSSH
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# install-innosetup.ps1 — robust silent install of Inno Setup 6 for the Windows runner.
|
||||
# Idempotent: exits 0 immediately if ISCC.exe is already present.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$candidates = @(
|
||||
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
|
||||
"$env:ProgramFiles\Inno Setup 6\ISCC.exe"
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path $c) { Write-Output "ISCC already present: $c"; exit 0 }
|
||||
}
|
||||
|
||||
$url = 'https://jrsoftware.org/download.php/is.exe'
|
||||
$out = Join-Path $env:TEMP 'innosetup-installer.exe'
|
||||
|
||||
Write-Output "Downloading Inno Setup: $url"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
# -UseBasicParsing avoids the IE-engine dependency that fails under the SYSTEM account.
|
||||
Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing
|
||||
|
||||
$size = (Get-Item $out).Length
|
||||
Write-Output "Downloaded $size bytes"
|
||||
if ($size -lt 1000000) { throw "Downloaded file too small ($size bytes) - not a valid installer" }
|
||||
|
||||
Write-Output "Installing silently..."
|
||||
$log = Join-Path $env:TEMP 'innosetup-install.log'
|
||||
$p = Start-Process -FilePath $out -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/SP-','/NOICONS',"/LOG=$log" -Wait -PassThru
|
||||
Write-Output "Installer exit code: $($p.ExitCode)"
|
||||
if ($p.ExitCode -ne 0) {
|
||||
Write-Output "----- install log tail -----"
|
||||
if (Test-Path $log) { Get-Content $log -Tail 40 | ForEach-Object { Write-Output $_ } }
|
||||
throw "Inno Setup installer failed with exit code $($p.ExitCode)"
|
||||
}
|
||||
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path $c) { Write-Output "ISCC installed: $c"; exit 0 }
|
||||
}
|
||||
throw "ISCC not found after install (looked in: $($candidates -join ', '))"
|
||||
@@ -2,8 +2,10 @@
|
||||
# provision-windows.sh — idempotent provisioning for the Windows host runner.
|
||||
#
|
||||
# Pre-warms what compile-windows.sh would download at runtime: Flutter Windows
|
||||
# engine artifacts and pub packages. Short-circuits via a stamp file keyed to
|
||||
# the installed Flutter version, so repeat runs return immediately.
|
||||
# engine artifacts, pub packages, and the Inno Setup compiler (ISCC) used to
|
||||
# build the installer. The Flutter part short-circuits via a stamp keyed to the
|
||||
# installed Flutter version; Inno Setup is ensured on every run (cheap when
|
||||
# already present) so it self-heals independently of the Flutter stamp.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
@@ -15,7 +17,23 @@ STAMP="${STAMP_DIR}/provisioned-windows"
|
||||
CUR="${STAMP_DIR}/.flutter-version-win.cur"
|
||||
mkdir -p "$STAMP_DIR"
|
||||
|
||||
# --- detection ---
|
||||
# --- ensure Inno Setup (ISCC) for building the installer (always checked) ---
|
||||
# ISCC installs to Program Files (x86) (32-bit app); check both to be safe.
|
||||
ISCC_X86="/c/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
||||
ISCC_X64="/c/Program Files/Inno Setup 6/ISCC.exe"
|
||||
if [ ! -f "$ISCC_X86" ] && [ ! -f "$ISCC_X64" ]; then
|
||||
echo "==> installing Inno Setup 6 (silent)"
|
||||
# Robust installer (checks download size + installer exit code) — inline
|
||||
# bash->powershell quoting was the original failure; a .ps1 avoids it.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "${SCRIPT_DIR}/install-innosetup.ps1"
|
||||
if [ ! -f "$ISCC_X86" ] && [ ! -f "$ISCC_X64" ]; then
|
||||
echo "ERROR: Inno Setup install failed — ISCC not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> Inno Setup installed."
|
||||
fi
|
||||
|
||||
# --- detection (Flutter) ---
|
||||
if ! command -v flutter >/dev/null 2>&1; then
|
||||
echo "ERROR: flutter not found on PATH. Install Flutter on this runner first." >&2
|
||||
exit 1
|
||||
|
||||
@@ -44,9 +44,13 @@ notes = sys.argv[2]
|
||||
tag = sys.argv[3]
|
||||
furl = sys.argv[4].rstrip('/')
|
||||
repo = sys.argv[5]
|
||||
base = f"{furl}/{repo}/releases/download/{tag}"
|
||||
# Public download URLs are served by jiu.51yanmei.com (same-origin HTTPS via the
|
||||
# pangolin nginx /downloads/ location), NOT the LAN-only HTTP Forgejo instance —
|
||||
# otherwise the HTTPS download page hands the browser an http://192.168.x URL and
|
||||
# Chrome blocks it as insecure / it's unreachable from the public internet.
|
||||
base = "https://jiu.51yanmei.com/downloads"
|
||||
mac_url = f"{base}/jiu-macos-x64.zip"
|
||||
win_url = f"{base}/jiu-windows-x64.zip"
|
||||
win_url = f"{base}/jiu-windows-x64-setup.exe"
|
||||
|
||||
lines = []
|
||||
with open('backend/config/version.yaml') as f:
|
||||
@@ -128,6 +132,6 @@ upload_asset dist/web.tar.gz
|
||||
upload_asset dist/marketing.tar.gz
|
||||
upload_asset dist/configs.tar.gz
|
||||
upload_asset dist/jiu-macos-x64.zip
|
||||
upload_asset dist/jiu-windows-x64.zip
|
||||
upload_asset dist/jiu-windows-x64-setup.exe
|
||||
|
||||
echo "==> release: done — Release ${TAG} created"
|
||||
|
||||
Reference in New Issue
Block a user