chore: 初始化项目基础结构和资源文件

- 添加项目图标文件(app-icon.png、各平台图标)
- 配置开发环境文件(.env、.nvmrc、.npmrc)
- 添加静态资源文件(背景图片、字体、音频)
- 初始化Tauri后端结构(build.rs、main.rs、模块文件)
- 配置前端项目结构(TypeScript、Vue组件、样式)
- 添加Node.js API服务基础结构
- 配置构建和开发工具(vite、prettier、gitignore)
This commit is contained in:
2026-03-13 10:03:05 +08:00
commit 78af453fe1
357 changed files with 70605 additions and 0 deletions

129
vite/auto-import/index.ts Normal file
View File

@ -0,0 +1,129 @@
import type {
Callback,
ElMessageBoxOptions,
IElMessageBox,
LoadingInstance,
LoadingOptions,
Message,
MessageBoxData,
MessageHandler,
MessageParams,
} from 'element-plus';
import { playPromptTone } from '@/utils/prompt-tone';
import type { AppContext } from 'vue';
declare const ElMessage: Message;
declare const ElLoading: { service: { (options?: LoadingOptions, context?: AppContext | null): LoadingInstance; _context: AppContext | null } };
declare const ElMessageBox: IElMessageBox;
/**
* toast弹出框
*/
export const Toast = {
_show: (message: string, type: 'error' | 'info' | 'success' | 'warning', duration = 3000): MessageHandler => {
return ElMessage({ showClose: true, grouping: true, message, type, duration });
},
show: (obj: MessageParams): MessageHandler => ElMessage(obj),
info: (message: string, duration?: number) => Toast._show(message, 'info', duration),
success: (message: string, duration?: number) => Toast._show(message, 'success', duration),
warning: (message: string, duration?: number) => Toast._show(message, 'warning', duration),
error: (message: string, duration?: number) => Toast._show(message, 'error', duration),
};
/**
* 全屏加载动画
*/
export const FullLoading = {
loadingInstance: null as { [key: string]: unknown; close: () => void } | null,
show(text: string, options = {}) {
this.loadingInstance = ElLoading.service({
lock: true,
fullscreen: true,
...options,
text,
});
},
hide() {
this.loadingInstance && this.loadingInstance.close();
},
};
export const MessageBox = {
MessageBox: (op: ElMessageBoxOptions = {}): Promise<MessageBoxData> => {
if ('callback' in op && typeof op.callback === 'function') {
return ElMessageBox(getElMessageBoxOptions(op));
} else {
try {
return ElMessageBox(getElMessageBoxOptions(op));
} catch (error: any) {
return Promise.reject(error);
} finally {
!('beforeClose' in op) && playPromptTone();
}
}
},
async alert(message: string, title?: string, op: ElMessageBoxOptions = {}): Promise<MessageBoxData> {
if ('callback' in op && typeof op.callback === 'function') {
return ElMessageBox.alert(message, title, getElMessageBoxOptions(op));
} else {
try {
const res = await ElMessageBox.alert(message, title, getElMessageBoxOptions(op));
return Promise.resolve(res);
} catch (error: any) {
return Promise.reject(error);
} finally {
!('beforeClose' in op) && playPromptTone();
}
}
},
async confirm(message: string, title?: string, op: ElMessageBoxOptions = {}): Promise<MessageBoxData> {
if ('callback' in op && typeof op.callback === 'function') {
return ElMessageBox.confirm(message, title, getElMessageBoxOptions(op));
} else {
try {
const res = await ElMessageBox.confirm(message, title, getElMessageBoxOptions(op));
return Promise.resolve(res);
} catch (error: any) {
return Promise.reject(error);
} finally {
!('beforeClose' in op) && playPromptTone();
}
}
},
async prompt(message: string, title?: string, op: ElMessageBoxOptions = {}): Promise<MessageBoxData> {
if ('callback' in op && typeof op.callback === 'function') {
return ElMessageBox.prompt(message, title, getElMessageBoxOptions(op));
} else {
try {
const res = await ElMessageBox.prompt(message, title, getElMessageBoxOptions(op));
return Promise.resolve(res);
} catch (error: any) {
return Promise.reject(error);
} finally {
!('beforeClose' in op) && playPromptTone();
}
}
},
};
/**
* 得到参数
*/
function getElMessageBoxOptions(op: ElMessageBoxOptions = {}) {
const newOp = { ...op };
if ('beforeClose' in op && typeof op.beforeClose === 'function') {
newOp.beforeClose = (...args: Parameters<NonNullable<ElMessageBoxOptions['beforeClose']>>) => {
playPromptTone();
op?.beforeClose?.(...args);
};
}
if ('callback' in op && typeof op.callback === 'function') {
newOp.callback = (...args: Parameters<Callback>) => {
!('beforeClose' in op) && playPromptTone();
// @ts-expect-error: Unreachable code error
op?.callback?.(...args);
};
}
return newOp;
}

15
vite/compression.ts Normal file
View File

@ -0,0 +1,15 @@
import type { PluginOption } from 'vite';
import { viteCompressionPlugin } from 'vite-auto-deploy';
/** 压缩 */
export function useViteCompression(isBuild: boolean): PluginOption {
if (isBuild) {
return viteCompressionPlugin({
verbose: false,
threshold: 1024 * 10,
algorithm: 'gzip',
}) as PluginOption;
} else {
return {} as PluginOption;
}
}

20
vite/cssOption.ts Normal file
View File

@ -0,0 +1,20 @@
import type { CSSOptions } from 'vite';
/**
* css的配置项目
* @description 如果是导入scss则注入scss的亮色主题
* @param importCssType - 导入css还是scss
*/
export function cssOption(importCssType: 'css' | 'scss'): CSSOptions {
const scssImportStr = `
@use "@/styles/vite/var-colors.scss" as *;
@use "@/styles/vite/mixin.scss" as *;`;
return {
preprocessorOptions: {
scss: {
additionalData: importCssType === 'scss' ? scssImportStr : scssImportStr, // scss注入
},
},
};
}

View File

@ -0,0 +1,86 @@
import type { Plugin } from 'vite';
/**
* 创建开发模式专用的条件插件
* 通过检查模块 ID 来决定是否应用插件
* @param plugin - 要包装的插件
* @param shouldApply - 判断是否应用的函数,接收模块 ID
*/
export function createDevConditionalPlugin(plugin: Plugin, shouldApply: (id: string) => boolean): Plugin {
if (!plugin || typeof plugin !== 'object') {
return plugin;
}
return {
...plugin,
name: `dev-conditional:${plugin.name}`,
// 在 transform 阶段检查
transform(code: string, id: string) {
// 如果不应该应用此插件,直接返回 null
if (!shouldApply(id)) {
return null;
}
// 否则调用原插件的 transform
if (typeof plugin.transform === 'function') {
return plugin.transform.call(this, code, id);
}
return null;
},
// 在 transformIndexHtml 阶段检查
transformIndexHtml(html: string, ctx: any) {
// 检查当前处理的 HTML 文件路径
const htmlPath = ctx.filename || ctx.path || ctx.originalUrl || '';
// 如果是简化入口的 HTML,跳过此插件
if (htmlPath.includes('floating-list-window') || htmlPath.includes('popup-window')) {
return html;
}
// 主入口或其他情况,调用原插件的 transformIndexHtml
if (typeof plugin.transformIndexHtml === 'function') {
return plugin.transformIndexHtml.call(this, html, ctx);
}
return html;
},
// 在 resolveId 阶段检查
resolveId(source: string, importer: string | undefined, options: any) {
// 如果导入者是简化入口,跳过此插件
if (importer && !shouldApply(importer)) {
return null;
}
// 否则调用原插件的 resolveId
if (typeof plugin.resolveId === 'function') {
return plugin.resolveId.call(this, source, importer, options);
}
return null;
},
// 在 load 阶段检查
load(id: string) {
if (!shouldApply(id)) {
return null;
}
if (typeof plugin.load === 'function') {
return plugin.load.call(this, id);
}
return null;
},
};
}
/**
* 判断模块是否为主入口(非 other_pages)
*/
export function isMainEntry(id: string): boolean {
// 排除 other_pages 目录下的所有文件
if (id.includes('other_pages') || id.includes('floating-list-window') || id.includes('popup-window')) {
return false;
}
return true;
}

74
vite/optimizeDeps.ts Normal file
View File

@ -0,0 +1,74 @@
import fs from 'fs';
/** 预构建的依赖 */
export async function elementPlusStyleOptimizeIncludes(type: 'css' | 'scss'): Promise<string[]> {
try {
const elementPlusStylePath: string[] = [];
// 得到 'element-plus/es/components' 目录下的所有目录名称
const dirnames = fs.readdirSync('node_modules/element-plus/es/components');
//得到需要检测的路径,防止导入不存在的路径
const accessPaths = dirnames.map((dirname) => {
const returnPath = `element-plus/es/components/${dirname}/style/${type === 'css' ? 'css' : 'index'}`; // 需要返回的路径
const checkPath = `node_modules/${returnPath}.mjs`; // 需要检测的路径
return getAccessPath(checkPath, returnPath);
});
// 得到所有路径包含null
const paths = await Promise.all(accessPaths);
// 去除null
paths.forEach((path) => {
// oxlint-disable-next-line no-unused-expressions
path && elementPlusStylePath.push(path);
});
return Promise.resolve(elementPlusStylePath);
// oxlint-disable-next-line no-unused-vars
} catch (error: any) {
return Promise.resolve([]);
}
}
/** 预构建的依赖 */
export async function optimizeIncludes(packages: string[]): Promise<string[]> {
const includesPaths: string[] = [];
//得到需要检测的路径,防止导入不存在的路径
const accessPaths = packages.map((dirname) => {
return getAccessPath(`node_modules/${dirname}`, dirname);
});
// 得到所有路径包含null
const paths = await Promise.all(accessPaths);
// 去除null
paths.forEach((path) => {
// oxlint-disable-next-line no-unused-expressions
path && includesPaths.push(path);
});
if (includesPaths.length !== packages.length) {
const notPath = packages.filter((v) => includesPaths.indexOf(v) === -1);
console.log('以下依赖没有被预构建:', notPath);
}
return Promise.resolve(includesPaths);
}
/**
* 检测文件是否存在如存在则返回对应路径如不存在则返回null
* @param checkPath - 需要检测的路径
* @param returnPath - 需要返回的路径
*/
function getAccessPath(checkPath: string, returnPath: string): Promise<string | null> {
return new Promise((resolve) => {
fs.access(checkPath, (err) => {
if (!err) {
resolve(returnPath);
} else {
fs.access(`${checkPath}.js`, (err2) => {
if (!err2) {
resolve(returnPath);
} else {
resolve(null);
}
});
}
});
});
}

39
vite/private-config.ts Normal file
View File

@ -0,0 +1,39 @@
import { isObject } from '../src/utils/verify';
import fs from 'fs';
import path from 'path';
import machineId from 'node-machine-id';
interface PrivateConfig {
/** 是否注册VueDevTools组件 */
isUseVueDevTools?: boolean;
/** 设备id */
deviceID?: string;
}
/**
* 获取或创建设备标识
*/
export function getPrivateConfig(): PrivateConfig {
const CONFIG_FILE = path.resolve(process.cwd(), '.MyPrivateConfig');
try {
const config: PrivateConfig = { isUseVueDevTools: true };
// 2. 尝试读取本地存储
if (fs.existsSync(CONFIG_FILE)) {
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
try {
let json: PrivateConfig = JSON.parse(content);
json = isObject(json) ? json : {};
config.isUseVueDevTools = json.isUseVueDevTools ?? true;
config.deviceID = json.deviceID ?? machineId.machineIdSync(true) ?? `${Date.now()}${Math.floor(Math.random() * 10000000000)}`;
// oxlint-disable-next-line no-unused-vars
} catch (error: any) {}
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
return config;
} catch (error: any) {
console.error('Device ID management error:', error);
return {};
}
}

48
vite/rolldownOptions.ts Normal file
View File

@ -0,0 +1,48 @@
import type { BuildOptions } from 'vite';
/**
* 自定义底层的 rolldown 打包配置
*/
export function rolldownOptions(): BuildOptions['rolldownOptions'] {
return {
output: {
// 拆分代码
manualChunks: (id: string) => {
const lastPath = id;
if (lastPath.includes('node_modules')) {
if (lastPath.includes('pinia')) {
return 'store';
}
if (lastPath.includes('element-plus')) {
return 'element-plus';
}
if (lastPath.includes('echarts')) {
return 'echarts';
}
if (lastPath.includes('vue')) {
return 'vue';
}
}
return undefined;
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
// assetFileNames: '[ext]/[name]-[hash].[ext]',
assetFileNames: (assetInfo) => {
const ext: string = assetInfo.name?.split('.').pop() || '';
let extType: string = ext;
if (['mp4', 'webm', 'ogg', 'mp3', 'wav', 'flac', 'aac'].includes(ext)) {
extType = 'media';
} else if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico'].includes(ext)) {
extType = 'images';
} else if (['woff', 'woff2', 'ttf', 'eot', 'otf'].includes(ext)) {
extType = 'fonts';
} else if (ext === 'css') {
extType = 'css';
} else {
extType = 'assets';
}
return `${extType}/[name]-[hash].[ext]`;
},
},
};
}

View File

@ -0,0 +1,24 @@
import { fileURLToPath } from 'url';
import path from 'path';
import { viteAutoDeply } from 'vite-auto-deploy';
import { platform } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, '..', '..');
const platformName = platform();
const osName = platformName === 'win32' ? 'windows' : platformName === 'darwin' ? 'mac' : platformName === 'linux' ? 'linux' : platformName;
/** 自动上传 */
viteAutoDeply({
uploadUrl: 'http://47.109.17.238:8200/api/upload/code/deploy',
projectKey: `tauri_interaction_class_${osName}`,
outDir: path.join(projectRoot, 'src-tauri', 'target', 'release', 'bundle'),
headers: {
'Custom-Timestamp': new Date().getTime().toString(),
'Custom-Run-Platform': 'app',
'Custom-Device-Id': Math.random().toString(36).substring(2),
'Custom-Os-Name': 'windows',
},
});

View File

@ -0,0 +1,544 @@
/* eslint-disable no-param-reassign */
/* eslint-disable no-template-curly-in-string */
/**
* 用于批量翻译 Tauri Schema 文件中的文件
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* 翻译函数
*/
function translateCommandDescription(text: string) {
if (typeof text !== 'string') {
return text;
}
// 处理 "Enables the X command without any pre-configured scope." 模式
const enablesPattern = /Enables the ([\w_]+) command without any pre-configured scope\./g;
text = text.replace(enablesPattern, (match, command) => {
return `在没有任何预配置范围的情况下启用 ${command} 命令。`;
});
// 处理 "Denies the X command without any pre-configured scope." 模式
const deniesPattern = /Denies the ([\w_]+) command without any pre-configured scope\./g;
text = text.replace(deniesPattern, (match, command) => {
return `在没有任何预配置范围的情况下拒绝 ${command} 命令。`;
});
return text;
}
/**
* 翻译函数
*/
function translateSchemaDescription(text: string) {
if (typeof text !== 'string') {
return text;
}
// 先处理多行描述 - 将换行符规范化
text = text.replace(/\\n/g, '\n');
// 处理实际换行符的多行描述
text = text.replace(
/This denies read access to the\n`\$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here\.\nAllowing access can lead to sensitive information disclosure and should be well considered\./gs,
'这拒绝读取 Linux 上的 `$APPLOCALDATA` 文件夹,因为 webview 数据和配置值存储在这里。允许访问可能导致敏感信息泄露,应仔细考虑。'
);
text = text.replace(
/This denies read access to the\n`\$APPLOCALDATA\/EBWebView` folder on windows as the webview data and configuration values are stored here\.\nAllowing access can lead to sensitive information disclosure and should be well considered\./gs,
'这拒绝读取 Windows 上的 `$APPLOCALDATA/EBWebView` 文件夹,因为 webview 数据和配置值存储在这里。允许访问可能导致敏感信息泄露,应仔细考虑。'
);
// 处理带 JSON 示例的长描述
text = text.replace(
/An empty permission you can use to modify the global scope\.\n\n## Example\n\n```json\n{\n {2}"identifier": "read-documents",\n {2}"windows": \["main"\],\n {2}"permissions": \[\n {4}"fs:allow-read",\n {3} {\n {6}"identifier": "fs:scope",\n {6}"allow": \[\n {8}"\$APPDATA\/documents\/\*\*\/\*"\n {6}\],\n {6}"deny": \[\n {8}"\$APPDATA\/documents\/secret\.txt"\n {6}\]\n {4}}\n {2}\]\n}\n```\n/gs,
'一个可用于修改全局范围的空权限。\n\n## 示例\n\n```json\n{\n "identifier": "read-documents",\n "windows": ["main"],\n "permissions": [\n "fs:allow-read",\n {\n "identifier": "fs:scope",\n "allow": [\n "$APPDATA/documents/**/*"\n ],\n "deny": [\n "$APPDATA/documents/secret.txt"\n ]\n }\n ]\n}\n```\n'
);
// 先应用正则表达式替换(通用模式)
const regexReplacements = [
[
/This allows non-recursive read access to metadata of the `(\$[^`]+)` folder, including file listing and statistics\./g,
'这允许对 `$1` 文件夹的元数据进行非递归读取访问,包括文件列表和统计信息。',
],
[
/This allows full recursive read access to metadata of the `(\$[^`]+)` folder, including file listing and statistics\./g,
'这允许对 `$1` 文件夹的元数据进行完全递归读取访问,包括文件列表和统计信息。',
],
[/This allows non-recursive read access to the `(\$[^`]+)` folder\./g, '这允许对 `$1` 文件夹进行非递归读取访问。'],
[
/This allows full recursive read access to the complete `(\$[^`]+)` folder, files and subdirectories\./g,
'这允许对完整的 `$1` 文件夹、文件和子目录进行完全递归读取访问。',
],
[/This allows non-recursive write access to the `(\$[^`]+)` folder\./g, '这允许对 `$1` 文件夹进行非递归写入访问。'],
[
/This allows full recursive write access to the complete `(\$[^`]+)` folder, files and subdirectories\./g,
'这允许对完整的 `$1` 文件夹、文件和子目录进行完全递归写入访问。',
],
// 新增模式
[
/This denies read access to the `\$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here\. Allowing access can lead to sensitive information disclosure and should be well considered\./gs,
'这拒绝读取 Linux 上的 `$APPLOCALDATA` 文件夹,因为 webview 数据和配置值存储在这里。允许访问可能导致敏感信息泄露,应仔细考虑。',
],
[
/This denies read access to the `\$APPLOCALDATA\/EBWebView` folder on windows as the webview data and configuration values are stored here\. Allowing access can lead to sensitive information disclosure and should be well considered\./gs,
'这拒绝读取 Windows 上的 `$APPLOCALDATA/EBWebView` 文件夹,因为 webview 数据和配置值存储在这里。允许访问可能导致敏感信息泄露,应仔细考虑。',
],
[/This enables all read related commands without any pre-configured accessible paths\./g, '这在没有任何预配置可访问路径的情况下启用所有与读取相关的命令。'],
[
/This enables directory read and file metadata related commands without any pre-configured accessible paths\./g,
'这在没有任何预配置可访问路径的情况下启用目录读取和文件元数据相关命令。',
],
[/This enables file read related commands without any pre-configured accessible paths\./g, '这在没有任何预配置可访问路径的情况下启用文件读取相关命令。'],
[
/This enables all index or metadata related commands without any pre-configured accessible paths\./g,
'这在没有任何预配置可访问路径的情况下启用所有索引或元数据相关命令。',
],
[
/This enables all write related commands without any pre-configured accessible paths\./g,
'这在没有任何预配置可访问路径的情况下启用所有与写入相关的命令。',
],
[
/This enables all file write related commands without any pre-configured accessible paths\./g,
'这在没有任何预配置可访问路径的情况下启用所有文件写入相关命令。',
],
[
/This scope permits access to all files and list content of top level directories in the application folders\./g,
'此范围允许访问应用程序文件夹中所有文件和顶级目录的内容列表。',
],
[/This scope permits to list all files and folders in the application directories\./g, '此范围允许列出应用程序目录中的所有文件和文件夹。'],
[
/This scope permits recursive access to the complete application folders, including sub directories and files\./g,
'此范围允许对完整的应用程序文件夹进行递归访问,包括子目录和文件。',
],
[
/This scope permits access to all files and list content of top level directories in the `(\$[^`]+)` folder\./g,
'此范围允许访问 `$1` 文件夹中所有文件和顶级目录的内容列表。',
],
[/This scope permits to list all files and folders in the `(\$[^`]+)` folder\./g, '此范围允许列出 `$1` 文件夹中的所有文件和文件夹。'],
[/This scope permits to list all files and folders in the `(\$[^`]+)`folder\./g, '此范围允许列出 `$1` 文件夹中的所有文件和文件夹。'],
[
/This scope permits recursive access to the complete `(\$[^`]+)` folder, including sub directories and files\./g,
'此范围允许对完整的 `$1` 文件夹进行递归访问,包括子目录和文件。',
],
[/This scope permits to list all files and folders in the `(\$[^`]+)` folder\./g, '此范围允许列出 `$1` 文件夹中的所有文件和文件夹。'],
];
for (const [pattern, replacement] of regexReplacements) {
text = text.replace(pattern!, replacement as any);
}
const translations = {
'Capability formats accepted in a capability file.': '能力文件中接受的能力格式。',
'A single capability.': '单个能力。',
'A list of capabilities.': '能力列表。',
'The list of capabilities.': '能力列表。',
'Default core plugins set.': '默认核心插件集。',
'Default permissions for the plugin.': '插件的默认权限。',
'Default permissions for the plugin, which enables all commands.': '插件的默认权限,启用所有命令。',
'This permissions allows to create the application specific directories.': '此权限允许创建应用程序特定的目录。',
'This denies access to dangerous Tauri relevant files and folders by default.': '这默认拒绝访问危险的 Tauri 相关文件和文件夹。',
'Identifier of the permission or permission set.': '权限或权限集的标识符。',
'Data that defines what is allowed by the scope.': '定义范围允许内容的数据。',
'Data that defines what is denied by the scope. This should be prioritized by validation logic.': '定义范围拒绝内容的数据。这应该由验证逻辑优先处理。',
'All supported ACL values.': '所有支持的 ACL 值。',
'Represents a null JSON value.': '表示一个空的 JSON 值。',
'Represents a list of other [`Value`]s.': '表示其他 [`Value`] 的列表。',
'Represents a map of [`String`] keys to [`Value`]s.': '表示 [`String`] 键到 [`Value`] 的映射。',
'Represents a [`bool`].': '表示一个 [`bool`]。',
'Represents a valid ACL [`Number`].': '表示一个有效的 ACL [`Number`]。',
'Represents a [`String`].': '表示一个 [`String`]。',
'Represents an [`i64`].': '表示一个 [`i64`]。',
'Represents a [`f64`].': '表示一个 [`f64`]。',
'Permission identifier.': '权限标识符。',
'Permission identifier': '权限标识符',
'Platform target.': '平台目标。',
'Windows.': 'Windows。',
'Linux.': 'Linux。',
'Android.': 'Android。',
'macOS.': 'macOS。',
'Allows the log command': '允许 log 命令',
'Allows connecting and sending data to a WebSocket server': '允许连接到 WebSocket 服务器并发送数据',
'Identifier of the capability.': '能力的标识符。',
'Description of what the capability is intended to allow on associated windows.': '描述该能力旨在在关联窗口上允许的内容。',
'It should contain a description of what the grouped permissions should allow.': '它应包含有关分组权限应允许内容的描述。',
'Configure remote URLs that can use the capability permissions.': '配置可以使用此能力权限的远程 URL。',
'This setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.':
'此设置是可选的,默认未设置,因为我们的默认用例是内容从本地应用程序提供。',
':::caution Make sure you understand the security implications of providing remote sources with local system access. :::':
':::caution 确保你了解为远程源提供本地系统访问的安全影响。 :::',
'Whether this capability is enabled for local app URLs or not. Defaults to `true`.': '是否为本地应用URL启用此能力。默认为 `true`。',
'List of windows that are affected by this capability. Can be a glob pattern.': '受此能力影响的窗口列表。可以是 glob 模式。',
'If a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].':
'如果窗口标签匹配此列表中的任何模式,则该能力将在该窗口的所有 webview 上启用,无论 [`Self::webviews`] 的值如何。',
'On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.':
'在多 webview 窗口上,建议指定 [`Self::webviews`] 并省略 [`Self::windows`] 以实现细粒度访问控制。',
'List of webviews that are affected by this capability. Can be a glob pattern.': '受此能力影响的 webview 列表。可以是 glob 模式。',
"The capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].":
'该能力将在标签匹配此列表中任何模式的所有 webview 上启用,无论 webview 的窗口标签是否匹配 [`Self::windows`] 中的模式。',
'List of permissions attached to this capability.': '附加到此能力的权限列表。',
'Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.':
'必须以 `${plugin-name}:${permission-name}` 的形式包含插件名称作为前缀。',
'For commands directly implemented in the application itself only `${permission-name}` is required.':
'对于直接在应用程序本身中实现的命令,只需要 `${permission-name}`。',
'Limit which target platforms this capability applies to.': '限制此能力应用于哪些目标平台。',
'By default all platforms are targeted.': '默认情况下,所有平台都被定位。',
'Configuration for remote URLs that are associated with the capability.': '与能力关联的远程URL的配置。',
'Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).':
'此能力引用的使用 [URLPattern 标准](https://urlpattern.spec.whatwg.org/)的远程域。',
'An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.':
'[`Capability`] 中权限值的条目可以是原始权限 [`Identifier`] 或引用权限并扩展其范围的对象。',
'Reference a permission or permission set by identifier.': '通过标识符引用权限或权限集。',
'Reference a permission or permission set by identifier and extends its scope.': '通过标识符引用权限或权限集并扩展其范围。',
// 新增权限集描述
'This allows non-recursive read access to metadata of the application folders, including file listing and statistics.':
'这允许对应用程序文件夹的元数据进行非递归读取访问,包括文件列表和统计信息。',
'This allows full recursive read access to metadata of the application folders, including file listing and statistics.':
'这允许对应用程序文件夹的元数据进行完全递归读取访问,包括文件列表和统计信息。',
'This allows non-recursive read access to the application folders.': '这允许对应用程序文件夹进行非递归读取访问。',
'This allows full recursive read access to the complete application folders, files and subdirectories.':
'这允许对完整的应用程序文件夹、文件和子目录进行完全递归读取访问。',
'This allows non-recursive write access to the application folders.': '这允许对应用程序文件夹进行非递归写入访问。',
'This allows full recursive write access to the complete application folders, files and subdirectories.':
'这允许对完整的应用程序文件夹、文件和子目录进行完全递归写入访问。',
'This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.':
'这允许对 `$APPCACHE` 文件夹的元数据进行非递归读取访问,包括文件列表和统计信息。',
'This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.':
'这允许对 `$APPCACHE` 文件夹的元数据进行完全递归读取访问,包括文件列表和统计信息。',
'This allows non-recursive read access to the `$APPCACHE` folder.': '这允许对 `$APPCACHE` 文件夹进行非递归读取访问。',
'This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.':
'这允许对完整的 `$APPCACHE` 文件夹、文件和子目录进行完全递归读取访问。',
'This allows non-recursive write access to the `$APPCACHE` folder.': '这允许对 `$APPCACHE` 文件夹进行非递归写入访问。',
'This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.':
'这允许对完整的 `$APPCACHE` 文件夹、文件和子目录进行完全递归写入访问。',
// 多行权限集描述
'This set of permissions describes the what kind of': '这组权限描述了',
'file system access the `fs` plugin has enabled or denied by default.': '文件系统访问类型, `fs` 插件默认启用或拒绝的。',
'This default permission set enables read access to the': '此默认权限集启用对',
'application specific directories (AppConfig, AppData, AppLocalData, AppCache,': '应用程序特定目录(AppConfig、AppData、AppLocalData、AppCache、',
'AppLog) and all files and sub directories created in it.': 'AppLog)及其中创建的所有文件和子目录的读取访问。',
'The location of these directories depends on the operating system,': '这些目录的位置取决于运行应用程序的操作系统,',
'where the application is run.': '应用程序运行的位置。',
'In general these directories need to be manually created': '通常,这些目录需要应用程序手动创建',
'by the application at runtime, before accessing files or folders': '在运行时,然后才能访问其中的文件或文件夹。',
'in it is possible.': '。',
'Therefore, it is also allowed to create all of these folders via': '因此,也允许通过',
'the `mkdir` command.': '`mkdir` 命令创建所有这些文件夹。',
'#### Denied Permissions': '#### 拒绝的权限',
'This default permission set prevents access to critical components': '此默认权限集默认阻止对',
'of the Tauri application by default.': 'Tauri 应用程序关键组件的访问。',
'On Windows the webview data folder access is denied.': '在 Windows 上,webview 数据文件夹访问被拒绝。',
// 更多权限集描述
'This permission set configures the types of dialogs': '此权限集配置对话框类型',
'available from the dialog plugin.': '从 dialog 插件可用。',
'This permission set configures what kind of': '此权限集配置',
'fetch operations are available from the http plugin.': '从 http 插件可用的 fetch 操作类型。',
'This enables all fetch operations but does not': '这启用了所有 fetch 操作,但不',
'allow explicitly any origins to be fetched. This needs to': '明确允许获取任何源。这需要',
'be manually configured before usage.': '在使用之前手动配置。',
'This permission set configures which': '此权限集配置',
'operating system information are available': '可用的操作系统信息',
'to gather from the frontend.': '从前端收集。',
'All information except the host name are available.': '除主机名外的所有信息都可用。',
'process features are by default exposed.': '默认暴露的进程功能。',
'This enables to quit via `allow-exit` and restart via `allow-restart`': '这允许通过 `allow-exit` 退出和通过 `allow-restart`',
'the application.': '重启应用程序。',
'This permission set configures which kind of': '此权限集配置哪种',
'updater functions are exposed to the frontend.': '更新器功能暴露到前端。',
'The full workflow from checking for updates to installing them': '从检查更新到安装它们的完整工作流',
'is enabled.': '已启用。',
'operations are available from the window state plugin.': '从窗口状态插件可用的操作类型。',
'All operations are enabled by default.': '默认情况下所有操作都已启用。',
'This permission allows recursive read functionality on the application': '此权限允许在应用程序特定的基本目录上进行递归读取功能。',
'specific base directories.': '。',
'An empty permission you can use to modify the global scope.': '一个可用于修改全局范围的空权限。',
'No features are enabled by default, as we believe': '默认情况下不启用任何功能,因为我们认为',
'the shortcuts can be inherently dangerous and it is': '快捷键可能具有固有的危险性,并且',
'application specific if specific shortcuts should be': '应用程序特定的是否应该',
'registered or unregistered.': '注册或取消注册特定的快捷键。',
'#### This permission set includes:': '#### 此权限集包括:',
// Config translations
'Sets the window associated with this label to be the parent of the window to be created.': '将与此标签关联的窗口设置为要创建窗口的父窗口。',
'## Platform-specific': '## 平台特定',
'This sets the passed parent as an owner window to the window to be created.': '这将传递的父窗口设置为要创建窗口的所有者窗口。',
'From [MSDN owned windows docs]': '来自 [MSDN 拥有窗口文档]',
'An owned window is always above its owner in the z-order.': '拥有的窗口在 Z 顺序中始终位于其所有者之上。',
'The system automatically destroys an owned window when its owner is destroyed.': '当其所有者被销毁时,系统会自动销毁拥有的窗口。',
'An owned window is hidden when its owner is minimized.': '当其所有者最小化时,拥有的窗口被隐藏。',
'This makes the new window transient for parent, see': '这使新窗口成为父窗口的临时窗口,参见',
'This adds the window as a child of parent, see': '这将窗口作为父窗口的子窗口添加,参见',
// 新增 - FS 和 HTTP scope 相关
'FS scope entry.': 'FS 范围条目。',
'FS scope path pattern.': 'FS 范围路径模式。',
'A path that can be accessed by the webview when using the fs APIs.': '使用 fs API 时 webview 可以访问的路径。',
'The pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.':
'模式可以以解析为系统基础目录的变量开头。变量包括:`$AUDIO`、`$CACHE`、`$CONFIG`、`$DATA`、`$LOCALDATA`、`$DESKTOP`、`$DOCUMENT`、`$DOWNLOAD`、`$EXE`、`$FONT`、`$HOME`、`$PICTURE`、`$PUBLIC`、`$RUNTIME`、`$TEMPLATE`、`$VIDEO`、`$RESOURCE`、`$APP`、`$LOG`、`$TEMP`、`$APPCONFIG`、`$APPDATA`、`$APPLOCALDATA`、`$APPCACHE`、`$APPLOG`。',
'HTTP scope entry.': 'HTTP 范围条目。',
'A URL that can be accessed by the webview when using the HTTP APIs.': '使用 HTTP API 时 webview 可以访问的 URL。',
'Wildcards can be used following the URL pattern standard.': '可以按照 URL 模式标准使用通配符。',
'See [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.':
'有关更多信息,请参阅 [URL Pattern 规范](https://urlpattern.spec.whatwg.org/)。',
'Examples:': '示例:',
'- "https://*" : allows all HTTPS origin on port 443': '- "https://*" : 允许 443 端口上的所有 HTTPS 源',
'- "https://*:*" : allows all HTTPS origin on any port': '- "https://*:*" : 允许任何端口上的所有 HTTPS 源',
'- "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path':
'- "https://*.github.com/tauri-apps/tauri": 允许 "github.com" 的任何子域名访问 "tauri-apps/api" 路径',
'- "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/"':
'- "https://myapi.service.com/users/*": 允许访问以 "https://myapi.service.com/users/" 开头的任何 URL',
'A valid ACL number.': '有效的 ACL 数字。',
'MacOS.': 'macOS。',
// Config 新增翻译
'The app name.': '应用程序名称。',
'The application version.': '应用程序版本。',
'An optional string that can be used to describe the capability.': '可用于描述能力的可选字符串。',
'The list of permissions to be added into the capability.': '要添加到能力的权限列表。',
'The list of scopes to be added into the capability.': '要添加到能力的范围列表。',
'The target platform for the capability.': '能力的目标平台。',
'The list of remote domains to be added into the capability.': '要添加到能力的远程域列表。',
'Whether the capability is enabled for local app URLs.': '是否为本地应用 URL 启用该能力。',
'The list of window labels to be added into the capability.': '要添加到能力的窗口标签列表。',
'The list of webview labels to be added into the capability.': '要添加到能力的 webview 标签列表。',
'Whether the capability is active.': '能力是否处于活动状态。',
'X coordinate.': 'X 坐标。',
'Y coordinate.': 'Y 坐标。',
'RGB color as an array. Each value must be between 0 and 255.': 'RGB 颜色数组。每个值必须在 0 到 255 之间。',
'RGBA color as an array. Each value must be between 0 and 255.': 'RGBA 颜色数组。每个值必须在 0 到 255 之间。',
'RGB color array. Each value has a min value of 0 and a max value of 255.': 'RGB 颜色数组。每个值的最小值为 0,最大值为 255。',
'RGBA color array. Each value has a min value of 0 and a max value of 255.': 'RGBA 颜色数组。每个值的最小值为 0,最大值为 255。',
'Fluent UI style overlay scrollbars. **Windows only**': 'Fluent UI 样式的覆盖滚动条。**仅限 Windows**',
"Requires WebView2 runtime version 125.0.2535.41 or higher, doesn't work on older versions,":
'需要 WebView2 运行时版本 125.0.2535.41 或更高版本,在旧版本上不起作用,',
'see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541':
'参见 https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541',
'Inline list of CSP sources. Same as [`Self::List`] but concatenated with space separator.':
'CSP 源的内联列表。与 [`Self::List`] 相同,但使用空格分隔符连接。',
'List of CSP sources. The collection will be concatenated with space separator as CSP string.': 'CSP 源列表。集合将使用空格分隔符连接为 CSP 字符串。',
'Brownfield mode.': 'Brownfield 模式。',
'The': '这',
'Access-Control-Allow-Credentials response header tells the browser': 'Access-Control-Allow-Credentials 响应头告诉浏览器',
'whether the server allows cross-origin HTTP requests to include credentials.': '服务器是否允许跨域 HTTP 请求包含凭据。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials>',
'The Access-Control-Allow-Headers response header is used in response': 'Access-Control-Allow-Headers 响应头用于响应',
'to a preflight request which includes the Access-Control-Request-Headers': '包含 Access-Control-Request-Headers 的预检请求,',
'to indicate which HTTP headers can be used during the actual request.': '以指示在实际请求期间可以使用哪些 HTTP 头。',
'This header is required if the request has an Access-Control-Request-Headers header.': '如果请求有 Access-Control-Request-Headers 头,则需要此头。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers>':
'请参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers>',
'The Access-Control-Allow-Methods response header specifies one or more methods': 'Access-Control-Allow-Methods 响应头指定一个或多个方法',
'allowed when accessing a resource in response to a preflight request.': '在响应预检请求访问资源时允许。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods>':
'请参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods>',
'The Access-Control-Expose-Headers response header allows a server to indicate': 'Access-Control-Expose-Headers 响应头允许服务器指示',
'which response headers should be made available to scripts running in the browser,': '哪些响应头应提供给浏览器中运行的脚本,',
'in response to a cross-origin request.': '以响应跨域请求。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers>':
'请参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers>',
'The Access-Control-Max-Age response header indicates how long the results of a preflight request': 'Access-Control-Max-Age 响应头指示预检请求的结果',
'(i.e. the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers)':
'(即 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 头中包含的信息)',
'can be cached.': '可以缓存多久。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age>',
'The HTTP Cross-Origin-Embedder-Policy (COEP) response header configures embedding cross-origin resources in a document.':
'HTTP Cross-Origin-Embedder-Policy (COEP) 响应头配置将跨源资源嵌入到文档中。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy>',
'The HTTP Cross-Origin-Opener-Policy (COOP) response header allows you to ensure': 'HTTP Cross-Origin-Opener-Policy (COOP) 响应头允许您确保',
'a top-level document does not share a browsing context group with cross-origin documents.': '顶级文档不与跨源文档共享浏览上下文组。',
"COOP process-isolate your document and potential attackers can't access your global object if they open it in a popup,":
'COOP 将对您的文档进行进程隔离,如果潜在攻击者在弹出窗口中打开它,也无法访问您的全局对象,',
'preventing a set of cross-origin attacks dubbed XS-Leaks.': '从而防止一组被称为 XS-Leaks 的跨源攻击。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy>',
'The HTTP Cross-Origin-Resource-Policy response header conveys a desire': 'HTTP Cross-Origin-Resource-Policy 响应头传达',
'that the browser blocks no-cors cross-origin/cross-site requests to the given resource.': '浏览器应阻止对给定资源的 no-cors 跨源/跨站点请求的愿望。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy>',
'The HTTP Permissions-Policy header provides a mechanism to allow and deny': 'HTTP Permissions-Policy 头提供一种机制来允许和拒绝',
'use of browser features in a document or in any <iframe> elements in the document.': '在文档中或文档的任何 <iframe> 元素中使用浏览器功能。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy>',
'The Referrer-Policy HTTP header controls how much referrer information': 'Referrer-Policy HTTP 头控制多少引用信息',
'(sent via the Referer header) should be included with requests.': '(通过 Referer 头发送)应包含在请求中。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy>',
'The Timing-Allow-Origin response header specifies origins that are allowed': 'Timing-Allow-Origin 响应头指定允许的源',
'to see values of attributes retrieved via the Resource Timing API features,': '查看通过 Resource Timing API 功能检索的属性值,',
'which would otherwise be reported as zero due to cross-origin restrictions.': '否则由于跨源限制,这些值将被报告为零。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin>',
'The X-Content-Type-Options response HTTP header is a marker used by the server': 'X-Content-Type-Options 响应 HTTP 头是服务器使用的标记',
'to indicate that the MIME types advertised in the Content-Type headers should be followed and not changed.':
'用于指示应遵循 Content-Type 头中通告的 MIME 类型而不应更改。',
'The header allows you to avoid MIME type sniffing by saying that the MIME types are intentionally configured.':
'该头允许您通过说 MIME 类型是故意配置的来避免 MIME 类型嗅探。',
'See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options>':
'参阅 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options>',
};
// 应用翻译
for (const [en, zh] of Object.entries(translations)) {
text = text.split(en).join(zh);
}
return text;
}
/**
* 翻译对象
*/
function translateObject(obj: any) {
if (typeof obj === 'object' && obj !== null) {
if (Array.isArray(obj)) {
obj.forEach((item) => translateObject(item));
} else {
for (const key in obj) {
if (key === 'description' || key === 'markdownDescription') {
if (typeof obj[key] === 'string') {
obj[key] = translateCommandDescription(obj[key]);
obj[key] = translateSchemaDescription(obj[key]);
}
} else {
translateObject(obj[key]);
}
}
}
}
}
/**
* 翻译文件
*/
function translateFile(filePath: string) {
console.log(`正在翻译: ${filePath}`);
try {
// 读取文件
const content = fs.readFileSync(filePath, 'utf8');
const data = JSON.parse(content);
// 翻译
translateObject(data);
// 保存文件
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
console.log(`✓ 完成: ${filePath}`);
return true;
} catch (error: any) {
console.error(`✗ 翻译失败 ${filePath}: ${error.message}`);
return false;
}
}
/**
* 主函数
*/
function main() {
console.log('='.repeat(60));
console.log('Tauri Schema 中文翻译脚本');
console.log('='.repeat(60));
console.log('');
const basePath = path.join(__dirname, 'src-tauri', 'gen', 'schemas-zh');
const files = [
path.join(basePath, 'desktop-schema.json'),
path.join(basePath, 'windows-schema.json'),
path.join(basePath, 'acl-manifests.json'),
path.join(basePath, 'config.json'),
];
let successCount = 0;
let failCount = 0;
files.forEach((filePath) => {
if (fs.existsSync(filePath)) {
if (translateFile(filePath)) {
successCount++;
} else {
failCount++;
}
} else {
console.log(`⚠ 文件不存在: ${filePath}`);
failCount++;
}
});
console.log('');
console.log('='.repeat(60));
console.log(`翻译完成! 成功: ${successCount}, 失败: ${failCount}`);
console.log('='.repeat(60));
}
// 运行
main();

246
vite/scripts/build.ts Normal file
View File

@ -0,0 +1,246 @@
#!/usr/bin/env node
import { execSync, spawn } from 'child_process';
import { platform } from 'os';
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import {
APPLE_CERTIFICATE,
APPLE_CERTIFICATE_PASSWORD,
APPLE_ID,
APPLE_PASSWORD,
APPLE_SIGNING_IDENTITY,
APPLE_TEAM_ID,
TAURI_SIGNING_PRIVATE_KEY,
TAURI_SIGNING_PRIVATE_KEY_PASSWORD,
} from './utils/constant.ts';
// ==================== 解析命令行参数 ====================
// 从命令行参数中获取 --features 的值
const args: string[] = process.argv.slice(2);
const featuresIndex: number = args.findIndex((arg: string) => arg === '--features');
const buildFeature: string = featuresIndex !== -1 && args[featuresIndex + 1] ? (args[featuresIndex + 1] as string) : 'production';
// 根据 feature 设置对应的环境模式
const modeMap: Record<string, string> = {
production: 'production',
development: 'development',
prod_150_8080: 'prod_150_8080',
};
const buildMode: string = (buildFeature in modeMap ? modeMap[buildFeature as keyof typeof modeMap] : modeMap.production) || 'production';
console.log('\x1b[36m%s\x1b[0m', `构建配置: --features ${buildFeature}, --mode ${buildMode}`);
// ==================== updater签名环境变量配置 ====================
// 私匙
process.env.TAURI_SIGNING_PRIVATE_KEY = TAURI_SIGNING_PRIVATE_KEY;
// 私匙对应的密码,没有就不配置
process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD = TAURI_SIGNING_PRIVATE_KEY_PASSWORD;
// Mac 公证
if (platform() === 'darwin') {
/** 签名证书在钥匙串中的名称(签名标识) */
process.env.APPLE_SIGNING_IDENTITY = APPLE_SIGNING_IDENTITY;
/** 从钥匙串导出的 .p12 证书的 base64 字符串(适用于 CI 或没有本地证书时) */
process.env.APPLE_CERTIFICATE = APPLE_CERTIFICATE;
/** .p12 证书的密码。 */
process.env.APPLE_CERTIFICATE_PASSWORD = APPLE_CERTIFICATE_PASSWORD;
/** 你的 Apple 账号邮箱(用于公证) */
process.env.APPLE_ID = APPLE_ID;
/** Apple 账号的 App 专用密码(用于公证) */
process.env.APPLE_PASSWORD = APPLE_PASSWORD;
/** 你的 Apple 开发者团队 ID用于公证 */
process.env.APPLE_TEAM_ID = APPLE_TEAM_ID;
}
// 运行版本更新脚本,同步 tauri.conf.json 和 package.json 的版本号
console.log('\x1b[36m%s\x1b[0m', '正在同步版本号...');
try {
const updateVersionScriptPath = join('.', 'vite', 'scripts', 'update-version.ts');
execSync(`node "${updateVersionScriptPath}"`, { stdio: 'inherit' });
console.log('\x1b[32m%s\x1b[0m', '版本号同步完成');
} catch (error: any) {
console.error('\x1b[31m%s\x1b[0m', '版本号同步失败:', error.message);
process.exit(1);
}
// 读取更新后的 package.json 获取版本号
interface PackageJson {
version: string;
[key: string]: any;
}
const packageJsonPath = join('package.json');
let packageJson: PackageJson = {} as PackageJson;
try {
packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
} catch (error: any) {
console.error('\x1b[31m%s\x1b[0m', '读取 package.json 失败:', error.message);
process.exit(1);
}
// 获取当前版本号
const newVersion: string = packageJson.version;
// 在构建前删除 src-tauri/target/release/bundle 目录内容
const bundleDir = join('src-tauri', 'target', 'release', 'bundle');
if (existsSync(bundleDir)) {
console.log('\x1b[33m%s\x1b[0m', `正在删除 ${bundleDir} 目录内容...`);
rmSync(bundleDir, { recursive: true, force: true });
}
// 先执行 Vite 默认配置构建
console.log('\x1b[36m%s\x1b[0m', '正在构建 index.html入口...');
const viteBuild = spawn('vite', ['build', '--mode', buildMode], {
stdio: 'inherit',
shell: true,
env: { ...process.env },
});
viteBuild.on('close', (code) => {
if (code !== 0) {
console.error('\x1b[31m%s\x1b[0m', `Vite 打包 index.html失败退出码: ${code}`);
process.exit(code);
}
console.log('\x1b[36m%s\x1b[0m', '正在构建 其他.html入口...');
const viteSimplifiedBuild = spawn('vite', ['build', '--config', 'vite.simplified.config.ts', '--mode', buildMode], {
stdio: 'inherit',
shell: true,
env: { ...process.env },
});
viteSimplifiedBuild.on('close', (simplifiedCode) => {
if (simplifiedCode !== 0) {
console.error('\x1b[31m%s\x1b[0m', `Vite 打包 其他.html入口构建失败退出码: ${simplifiedCode}`);
process.exit(simplifiedCode);
}
console.log('\x1b[36m%s\x1b[0m', 'Vite 构建完成,开始执行 Tauri 构建...');
const tauriBuild = spawn('tauri', ['build', '--features', buildFeature], {
stdio: 'inherit',
shell: true,
env: process.env, // 传递所有环境变量
});
tauriBuild.on('close', (tauriCode) => {
if (tauriCode === 0) {
// 写入 latest.json 到 bundle 目录
const latestJsonPath = join(bundleDir, 'latest.json');
// 为不同平台生成相应的 latest.json
if (platform() === 'win32') {
const latestJson = getLatestJson('windows', newVersion, 'nsis', 'exe');
writeFileSync(latestJsonPath, JSON.stringify(latestJson, null, 2));
console.log('\x1b[32m%s\x1b[0m', `已生成 latest.json 文件: ${latestJsonPath}`);
} else if (platform() === 'darwin') {
const latestJson = getLatestJson('macos', newVersion, 'macos', 'app.tar.gz');
writeFileSync(latestJsonPath, JSON.stringify(latestJson, null, 2));
console.log('\x1b[32m%s\x1b[0m', `已生成 latest.json 文件: ${latestJsonPath}`);
} else if (platform() === 'linux') {
// 为 AppImage 生成 latest.json
const appImageLatest = getLatestJson('linux', newVersion, 'appimage', 'AppImage');
writeFileSync(join(bundleDir, 'appimage', 'latest.json'), JSON.stringify(appImageLatest, null, 2));
console.log('\x1b[32m%s\x1b[0m', `已生成 appimage-latest.json 文件`);
// 为 deb 生成 latest.json
const debLatest = getLatestJson('linux', newVersion, 'deb', 'deb');
writeFileSync(join(bundleDir, 'deb', 'latest.json'), JSON.stringify(debLatest, null, 2));
console.log('\x1b[32m%s\x1b[0m', `已生成 deb-latest.json 文件`);
// 为 rpm 生成 latest.json
const rpmLatest = getLatestJson('linux', newVersion, 'rpm', 'rpm');
writeFileSync(join(bundleDir, 'rpm', 'latest.json'), JSON.stringify(rpmLatest, null, 2));
console.log('\x1b[32m%s\x1b[0m', `已生成 rpm-latest.json 文件`);
}
// Tauri 构建成功后执行自动部署
console.log('\x1b[36m%s\x1b[0m', '开始执行自动部署...');
try {
const autoDeployScriptPath = join('.', 'vite', 'scripts', 'auto-deply.ts');
execSync(`npx tsx "${autoDeployScriptPath}"`, { stdio: 'inherit' });
console.log('\x1b[32m%s\x1b[0m', '自动部署执行完成');
} catch (error: any) {
console.error('\x1b[31m%s\x1b[0m', '自动部署执行失败:', error.message);
// 注意:这里不退出进程,因为构建本身是成功的
}
} else {
console.error('\x1b[31m%s\x1b[0m', `Tauri 构建失败,退出码: ${tauriCode}`);
process.exit(tauriCode);
}
});
});
});
/**
* 得到最新版本信息
* @param ostype - 平台类型,如 'windows'、'macos'、'linux'
* @param version - 新版本号
* @param catalog - 包的目录名
* @param extension - 包的扩展名
*/
interface LatestJson {
version: string;
pub_date: string;
url: string;
signature: string;
notes: string;
downloadLink?: string;
}
/**
* 获取最新版本信息
* @param ostype - 平台类型,如 'windows'、'macos'、'linux'
* @param version - 新版本号
* @param catalog - 包的目录名
* @param extension - 包的扩展名
*/
function getLatestJson(ostype: string, version: string, catalog: string, extension: string): LatestJson {
const pubDate = new Date().toISOString();
const latestJson: LatestJson = {
version,
pub_date: pubDate,
url: '',
signature: '',
notes: '1. 修复BUG \n 2. 优化部分功能',
};
const baseUrl = `https://file.qyzhjy.com/app/zpkt_desktop_app/${ostype}`;
try {
if (ostype === 'windows') {
const nsisDir = join(bundleDir, catalog);
const files = readdirSync(nsisDir);
const exeFile = files.find((file) => file.endsWith('exe'));
latestJson.downloadLink = `${baseUrl}/${catalog}/${exeFile}`;
} else if (ostype === 'macos') {
const dmgDir = join(bundleDir, 'dmg');
const files = readdirSync(dmgDir);
const dmgFile = files.find((file) => file.endsWith('dmg'));
latestJson.downloadLink = `${baseUrl}/dmg/${dmgFile}`;
} else if (ostype === 'linux') {
const appImageDir = join(bundleDir, catalog);
const files = readdirSync(appImageDir);
const appImageFile = files.find((file) => file.endsWith(extension));
latestJson.downloadLink = `${baseUrl}/${catalog}/${appImageFile}`;
}
// 查找 nsis 目录下的 exe 文件
const nsisDir = join(bundleDir, catalog);
if (existsSync(nsisDir)) {
const files = readdirSync(nsisDir);
const exeFile = files.find((file) => file.endsWith(extension));
const sigFile = files.find((file) => file.endsWith(`${extension}.sig`));
if (exeFile) {
// 设置 Windows 平台的 URL
latestJson.url = `${baseUrl}/${catalog}/${exeFile}`;
// 如果存在对应的 .sig 文件,读取签名内容
if (sigFile) {
const sigFilePath = join(nsisDir, sigFile);
latestJson.signature = readFileSync(sigFilePath, 'utf8').trim();
}
}
}
return latestJson;
} catch (error: any) {
console.warn('\x1b[33m%s\x1b[0m', '警告: 无法读取 Windows 安装包信息:', error.message);
return latestJson;
}
}

View File

@ -0,0 +1,56 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// 获取项目根目录和 src-tauri 目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, '..', '..');
const tauriConfigPath = path.join(projectRoot, 'src-tauri', 'tauri.conf.json');
const packageJsonPath = path.join(projectRoot, 'package.json');
try {
// 读取 tauri.conf.json
const tauriConfigRaw: string = fs.readFileSync(tauriConfigPath, 'utf8');
const tauriConfig: { version: string } = JSON.parse(tauriConfigRaw);
const tauriVersion: string = tauriConfig.version;
console.log(`从 tauri.conf.json 读取的版本号: ${tauriVersion}`);
// 计算新的版本号tauri版本号尾号加1
const newPackageVersion: string = incrementVersion(tauriVersion);
// 读取 package.json
const packageJsonRaw: string = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson: { version: string } = JSON.parse(packageJsonRaw);
// 更新 package.json 中的版本号
packageJson.version = newPackageVersion;
tauriConfig.version = newPackageVersion;
// 写入更新后的 package.json
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8');
fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), 'utf8');
console.log(`已更新 package.json 和 tauri.conf.json 的版本号为: ${newPackageVersion}`);
} catch (error: any) {
console.error('更新版本号时发生错误:', error);
process.exit(1);
}
/**
* 将版本号的最后一个数字部分加1
* @param version - 原始版本号,如 "1.0.10"
* @returns 更新后的版本号,如 "1.0.11"
*/
function incrementVersion(version: string): string {
const parts: string[] = version.split('.');
if (parts.length > 0) {
const lastPartIndex = parts.length - 1;
const lastPart = Number(parts[lastPartIndex]);
if (!isNaN(lastPart)) {
parts[lastPartIndex] = (lastPart + 1).toString();
return parts.join('.');
}
}
return version; // 如果无法解析版本号,则返回原版本号
}

View File

@ -0,0 +1,92 @@
/** 代码签名的密匙APP升级专用 */
export const TAURI_SIGNING_PRIVATE_KEY =
'dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YllCRHU1dk1ITW54UTViUUI5YnVmb0NQcTFkL09qQ0hSeHpOTmV2Z0ZtY0FBQkFBQUFBQUFBQUFBQUlBQUFBQVFCNGgrV0RsRGR5OU5uWnNKVkoxZWE4NlJLd1lhblYvWC9tRzNLbmpvMzFnYUtPMGJiRVBRQXpqVVFQWExuaGFaL2pPU3AyODhkdCtpTC9EamhBbTNDaGduTU9pMVRBZlRZa2dPSGk0cnk4b1h4YXZFRjgvYlZIb1NKSkxWSTdleXkveXlyQ3BMK3c9Cg==';
/** 代码签名的密码APP升级专用 */
export const TAURI_SIGNING_PRIVATE_KEY_PASSWORD = 'QYZHJYSRJY20250701';
/** 签名证书在钥匙串中的名称(签名标识) */
export const APPLE_SIGNING_IDENTITY = '744UKKLG8B';
/** 从钥匙串导出的 .p12 证书的 base64 字符串(适用于 CI 或没有本地证书时) */
export const APPLE_CERTIFICATE = `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
0nTF5Lzb/LSc3JIw7LLFd/u0bR8GwDjj3v2eWqJ0b9pYXxX69RiKPSpYIJjWQ3XU
E7WG/97s+FokK2at2Vt8LQDWfC0Cj1Wa+ob92mpDle31MuvayaHp0K17G2G94lcl
lbJTkBswB3Ng9ON65JrQqNfCjz8mp9ywvhBcDxi0UPuOt2gcotxcOTXYT2ThYttg
Tu4LHyhCMyBsuKCpto3J/DNBwsxAvpHEcfHn+Y2/N7D/OSkRJalGhujkopHXFA99
3ntzokiFOsfsCkV67xKfV7OHRSTrLr2mjDaV/EJwI7+S5Q2w8UiZtJqzX5Nl+1Fv
jUyrQKA2OuH1J5DkhipUHAisSQV4WzAoLET2GtIxiq8cG+lRje1DFi2+XIeQZzx9
863aCtIMfTC6v02Tpcx8l2Mbx3/LGw6/q+5MfNP41OJns/GupHR5dave2jipt5f4
Jus/DSRo7edatLupDHZAwy2QsDtyMUsAHEHye6yOUQOuCd67VF8WiyN/i/uCTQX9
9txNQhjZWzpOo/+m4eQWEJGinKHR+I/qalIYr2XdLaHdyMSMCV5vQPNiaKaHlC53
FW16Hy2aXY+f6P1V4BJIeh2d8ks08iAKyZYQ/3+Pttrcx+i/0dU9Vw/aneszMOip
oFaupPPsacv6YgZol2laWgoeoMdnF798hSMmeAJKoTJMBIAxgeEwgbkGCSqGSIb3
DQEJFDGBqx6BqABNAGEAYwAgAEQAZQB2AGUAbABvAHAAZQByACAASQBEACAAQQBw
AHAAbABpAGMAYQB0AGkAbwBuADoAIABDAGgAbwBuAGcAcQBpAG4AZwAgAFEAdQBu
AFkAdQBlACAAVwBpAHMAZABvAG0AIABFAGQAdQBjAGEAdABpAG8AbgAgAFQAZQBj
AGgAbgBvAGwAbwBnAHkAIABDAG8ALgAsACAATAB0AGQALjAjBgkqhkiG9w0BCRUx
FgQU4ezRLGC8n3cngopV/PcmcmEK6A4wMDAhMAkGBSsOAwIaBQAEFDJZ7gOUH9Z9
AjHY9VxdtKMirOHoBAheoU7cW3PopAIBAQ==
`;
/** .p12 证书的密码。 */
export const APPLE_CERTIFICATE_PASSWORD = 'ns7NH4YRGbPUsgLeEU3N';
/** 你的 Apple 账号邮箱(用于公证) */
export const APPLE_ID = 'qyzh@qyzhjy.com';
/** Apple 账号的 App 专用密码(用于公证) */
export const APPLE_PASSWORD = 'yhyu-lnah-juxf-mmtc';
/** 你的 Apple 开发者团队 ID用于公证 */
export const APPLE_TEAM_ID = '744UKKLG8B';

70
vite/svgIconPathOption.ts Normal file
View File

@ -0,0 +1,70 @@
// vite/vite-plugin-icon-dts.ts
import fs from 'fs-extra';
import glob from 'fast-glob';
import path from 'path';
import { useThrottleFn } from '@vueuse/core';
import type { PluginOption } from 'vite';
// glob 默认只支持 / 作为路径分隔符windows 下会出现问题
const normalizePath = (_path: string) => _path.replace(/\\/g, '/');
interface IconDtsOptions {
/** svg 图标文件的路径 */
iconDirs: string;
/** 输出的dts文件的路径 */
dts: string;
/** 监听变化的延迟时间 */
delay: number;
/** 接口名称 */
interfaceName: string;
}
const defualtOptions: IconDtsOptions = {
iconDirs: 'src/icons/',
dts: 'svg-icons.d.ts',
delay: 200,
interfaceName: 'BaseSvgIconPath',
};
/**
* svg 图标路径生成dts文件
*/
export function svgIconPathOption(options: Partial<IconDtsOptions> = {}): PluginOption {
const finalOptions: IconDtsOptions = { ...defualtOptions, ...options };
const { delay, interfaceName } = finalOptions;
let { iconDirs, dts } = finalOptions;
iconDirs = normalizePath(iconDirs);
dts = normalizePath(dts);
let watcher: fs.FSWatcher | undefined = undefined;
return {
name: 'svg-icons-dts',
buildStart: () => {
if (!fs.existsSync(iconDirs)) {
console.error(`${iconDirs}不存在,请检查`);
return;
}
const update = () => {
let assets: string[] = glob.sync(`${iconDirs}/**/*.svg`, {});
assets = assets.map((i: string) => i.replace(iconDirs, '').replace('.svg', '').replace(/\//g, '-').replace(/^-+/, ''));
let output = `interface ${interfaceName} {\n`;
assets.forEach((_item) => {
output += ` '${_item}': string;\n`;
});
output += `}\n`;
const base = path.dirname(dts);
fs.ensureDirSync(base);
fs.writeFileSync(dts, output);
};
const debounceLogic = useThrottleFn(update, delay);
// 监听到文件变化,就重新写一遍
watcher = fs.watch(iconDirs, { recursive: true }, () => {
debounceLogic();
});
update();
},
buildEnd: () => {
watcher?.close();
},
};
}

View File

@ -0,0 +1,57 @@
import type { PluginOption } from 'vite';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
/**
* 打包图片压缩
*/
export function viteImageminOption(): PluginOption {
return ViteImageOptimizer({
logStats: false,
gif: {},
png: {
quality: 90,
},
jpeg: {
quality: 90, // 设置 jpg 压缩质量
},
webp: {
quality: 80,
lossless: true,
},
svg: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
// 保留关键属性
removeViewBox: false, // 保持响应式能力
removeTitle: false, // 保留可访问性
removeDesc: false, // 保留可访问性
cleanupIds: false, // 防止ID被修改
collapseGroups: false, // 保留分组结构
// 优化配置
convertPathData: {
// 路径优化
floatPrecision: 2, // 保留2位小数精度
},
convertTransform: {
// 变换优化
floatPrecision: 2,
},
},
},
},
// 额外插件
'removeDimensions', // 移除冗余的width/height
'sortAttrs', // 属性排序提高压缩率
{
name: 'removeAttrs',
params: {
attrs: 'data-.+', // 保留所有data-属性
preserveCurrentColor: true,
},
},
],
},
});
}