切换框架tauri v1

master
cysamurai 2 months ago
parent 50157b0c46
commit 982660eefe

@ -1,343 +1,75 @@
# TauriClient
# TauriClient 打包与目录说明
多个 Tauri 2 + Vue 子项目的仓库,在 **Ubuntu 24.04.xx86_64** 上打 **amd64****arm64**`.deb` 使用统一脚本。
## 1. 打包命令流程
## 构建
推荐使用双容器模式(默认)同时产出 `amd64``arm64`
```bash
cd /path/to/TauriClient
chmod +x scripts/build-linux-deb-all.sh
./scripts/build-linux-deb-all.sh
```
- 不带参数:按 `scripts/build-linux-deb-all.sh``PROJECTS` 依次构建全部项目。
- 只构建部分项目:
```bash
./scripts/build-linux-deb-all.sh call-client
./scripts/build-linux-deb-all.sh call-client broadcast-client
```
在各子目录内也可只打**当前项目**的两种架构:
- `call-client`: `npm run build:deb:all`
- `broadcast-client`: `npm run build:deb:all`
## 本机 amd64 依赖Tauri 官方 Linux 前置)
在 x86_64 上打 amd64 包前,需安装与本机架构一致的开发包(名称以 [Tauri 前置依赖](https://tauri.app/start/prerequisites/) 为准),例如:
```bash
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev patchelf build-essential curl wget file libssl-dev \
libglib2.0-dev pkg-config
```
## 原生 arm64 真机 / 云主机(直接打 arm64`:arm64` / multiarch
**`gio-2.0.pc` / `glib-2.0.pc` 找不到**,提示里写 **`PKG_CONFIG_PATH`**:在**本机就是 arm64** 时,一般**不是**路径配错,而是 **没装 GLib/GTK/WebKit 的开发包**。请先装依赖(**不要**加 `:arm64` 后缀),再构建:
```bash
sudo apt update
sudo apt install -y \
build-essential pkg-config libssl-dev \
libglib2.0-dev \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libjavascriptcoregtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
curl file
```
自检(应能打印出路径且无报错):
```bash
pkg-config --exists gio-2.0 && pkg-config --cflags gio-2.0
ls /usr/lib/aarch64-linux-gnu/pkgconfig/gio-2.0.pc
```
然后在子项目目录执行 **`npm run build:deb:arm64`**(或 `tauri build --bundles deb --target aarch64-unknown-linux-gnu`)。**不要**在真机上照搬 x86 交叉编译时那套 `PKG_CONFIG_ALLOW_CROSS`、`PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig` 等环境变量,除非你知道自己在改什么——默认 `pkg-config` 即可。
仓库根目录的 **`build-linux-deb-all.sh`** 会先打 **amd64** 再打 **arm64**,适合 **x86_64 宿主机**;在 **纯 arm64 机器**上请**不要**直接跑该全量脚本(缺 amd64 工具链会失败),改为按项目单独 `npm run build:deb:arm64`
## 交叉编译 arm64multiarch + :arm64 开发包)
若在打 arm64 时出现 **`gobject-sys` / `glib-sys` / `gio-sys` 找不到 `glib-2.0.pc`** 等错误,说明宿主机上**没有安装 arm64 架构的 `-dev` 包**。仅设置 `PKG_CONFIG_*` 不够,必须安装带 **`:arm64`** 的包。
**顺序:先 `add-architecture`,再 `apt update`,最后 `apt install`。**
**重要:** 在 x86_64 宿主机上**不要**安装 **`systemd-sysv:arm64`**。它与本机 **`systemd-sysv`amd64在软件包元数据里互相 `Conflicts`**,无法共存;若 apt 因 **Recommends** 试图同时拉两边,就会解算失败。交叉编译只需 arm64 的 **库与 -dev**,应使用 **`--no-install-recommends`** 安装 `:arm64` 包,避免把 `systemd-sysv:arm64` 拉进来。
```bash
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install -y --no-install-recommends \
gcc-aarch64-linux-gnu \
pkg-config \
libglib2.0-dev:arm64 \
libgtk-3-dev:arm64 \
libcairo2-dev:arm64 \
libpango1.0-dev:arm64 \
libgdk-pixbuf-2.0-dev:arm64 \
libatk1.0-dev:arm64 \
libepoxy-dev:arm64 \
libwebkit2gtk-4.1-dev:arm64 \
libjavascriptcoregtk-4.1-dev:arm64 \
libssl-dev:arm64 \
libayatana-appindicator3-dev:arm64 \
librsvg2-dev:arm64 \
patchelf
```
### 若报 `gdk-3.0` / `gdk-sys` 找不到 `.pc`
`gdk-3.0.pc` 在 Ubuntu 上由 **`libgtk-3-dev:arm64`** 安装到 `/usr/lib/aarch64-linux-gnu/pkgconfig/`。若只装了 `libglib2.0-dev:arm64` 而没有装齐 GTK 栈,就会出现你看到的错误。执行上面的 `apt install`(至少包含 **`libgtk-3-dev:arm64`** 及 cairo/pango/atk 等),然后确认:
```bash
ls /usr/lib/aarch64-linux-gnu/pkgconfig/gdk-3.0.pc
```
### 若 `apt install …:arm64` 报依赖未满足(例如 `libpulse0:arm64``libapparmor1:arm64`
先修依赖、再显式装上缺的 arm64 基础库,然后重试安装 GTK/WebKit 开发包:
```bash
sudo apt --fix-broken install
sudo apt install -y --no-install-recommends libapparmor1:arm64
sudo apt install -y --no-install-recommends \
libgtk-3-dev:arm64 libcairo2-dev:arm64 libpango1.0-dev:arm64 \
libgdk-pixbuf-2.0-dev:arm64 libatk1.0-dev:arm64 libepoxy-dev:arm64 \
libwebkit2gtk-4.1-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64
```
若仍失败,查看 apt 是否拒绝安装 `libapparmor1:arm64` 的原因:
```bash
apt-cache policy libapparmor1:arm64
sudo apt install -o Debug::pkgProblemResolver=true libapparmor1:arm64
```
**提示:** 不要把 `ubuntu.sources.bak.*` 留在 `/etc/apt/sources.list.d/`(部分 apt 版本会提示扩展名无效);备份文件请移到 `$HOME` 等目录。
### 若安装任意 `:arm64` 时都报 `init` 预依赖 `systemd-sysv` / pkgProblemResolver 失败
说明 **本机 amd64 的 apt/dpkg 状态已异常**(或关键元包未装全),应先修主系统再装 multiarch。在虚拟机里可先做快照再执行
```bash
sudo apt update
sudo apt install -y systemd-sysv
sudo dpkg --configure -a
sudo apt --fix-broken install
```
仍报错时再查版本与是否被 hold
```bash
apt-cache policy init systemd-sysv
apt-mark showhold
dpkg -l | grep -E '^(..) (init|systemd-sysv) '
```
`systemd-sysv` 无法安装,需在能登录图形/SSH 的前提下对照上述输出排查(有时需 `sudo apt install --reinstall init systemd-sysv`**有风险**,优先快照)。
主系统长期无法通过 `apt install` 修复时,建议在 **原生 arm64 环境** 或 **arm64 容器** 内单独打 arm64 包,避免在损坏的 apt 上叠 multiarch。
### `systemd-sysv``systemd-sysv:arm64` 冲突(你看到的 Conflicts
在 amd64 宿主机上,**`systemd-sysv`amd64`systemd-sysv:arm64` 互斥**不能同时安装。若某次安装把两者放进同一笔交易apt 会报 **冲突** 或前面那种 **`init` 预依赖** 的误导性提示。
**正确做法:** 只保留本机 **amd64**`systemd-sysv`;装 arm64 的库 / `-dev` 时一律加 **`--no-install-recommends`**,且**不要**执行 `apt install systemd-sysv:arm64`
自测(`libc6:arm64` 应已可装):
```bash
sudo apt install -y --no-install-recommends libapparmor1:arm64
sudo apt install -y --no-install-recommends \
libgtk-3-dev:arm64 libwebkit2gtk-4.1-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64
```
若曾误装 `systemd-sysv:arm64`,先卸掉再装其它 arm64 包:
```bash
dpkg -l | grep systemd-sysv
# 若存在 systemd-sysv:arm64
sudo apt remove -y systemd-sysv:arm64
```
仍失败时保存解算日志:
```bash
sudo apt-get install -o Debug::pkgProblemResolver=true --no-install-recommends libapparmor1:arm64 2>&1 | tee ~/apt-arm64-debug.txt
```
**务实绕过:** 若 `--no-install-recommends` 仍因 **硬依赖** 拉进冲突包,在 **arm64 环境****Docker `--platform linux/arm64`** 内单独打 arm64 包(见下)。
```bash
docker run --rm -it --platform linux/arm64 -v "$PWD:/work" ubuntu:24.04 bash
```
在容器内按本文 **「本机 amd64 依赖」** 装一套 **arm64 的** `apt install`(无需 `:arm64` 后缀),再安装 Node、Rust 与项目依赖后执行 `npm run build:deb:arm64`。主机上继续用脚本打 **amd64** 即可。
### 故障排除:`apt` 提示「无法定位软件包 …:arm64」
说明当前 **apt 没有 arm64 的软件包索引**(与包名写错是两回事)。常见原因:
1. **未启用外架构或未刷新**
- 执行:`sudo dpkg --add-architecture arm64` 后必须再执行 `sudo apt update`
- 自检:`dpkg --print-foreign-architectures` 输出里应有 `arm64`
2. **`sources.list` 里限制了仅 amd64**VM / 公司镜像 / 手动改源时很常见)
若存在类似 `deb [arch=amd64] http://……` 且**没有** `arm64`,则 apt **不会拉 arm64 索引**。
**注意:** 若在**主归档**(如 `archive.ubuntu.com/ubuntu`、`mirrors.tuna…/ubuntu/`)上写 `[arch=amd64,arm64]`apt 会去拉 `dists/…/binary-arm64/`,而**主归档根本没有 arm64**(见下第 4 点)。正确做法是 **amd64 与 arm64 分开写两套 URI**DEB822 里用 `Architectures:`),而不是简单合并 `arch=`
3. **自检索引是否已包含 arm64**
```bash
apt-cache policy libc6:arm64
```
**Candidate 为 (none)**,说明索引仍不对,继续检查第 2 步;若已有版本号,再执行上面的 `apt install …:arm64`
4. **`apt update` 对 arm64 出现 `404``…/dists/noble/…/binary-arm64/Packages`**
**`archive.ubuntu.com/ubuntu` 与清华 `…/ubuntu/` 这类主归档不包含 arm64 的 `binary-arm64`。** 在 x86_64 上启用 `arm64` 做 multiarch 时,**arm64 必须走 ports** `http://ports.ubuntu.com/ubuntu-ports`(国内可用清华等 **ubuntu-ports** 镜像,见 [清华 ubuntu-ports](https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu-ports/))。
**`ubuntu.sources`** 里拆成多段,并写 **`Architectures:`**示例见下方「DEB822主归档 + ports」。**不要**指望把主源改成 `archive.ubuntu.com` 就能解决 arm64 404。
5. **`sources.list``ubuntu.sources` 重复(大量 “被配置了多次” 警告)**
Ubuntu 24.04 默认会用 **`/etc/apt/sources.list.d/ubuntu.sources`**DEB822 格式),若你又手工在 **`/etc/apt/sources.list`** 里写了同一套 `noble / noble-updates …`,两套会**重复指向同一组件**,产生警告且不利于排查。
**建议只保留一套:** 例如注释掉 `sources.list` 里全部 `deb` 行,只在 `ubuntu.sources` 里配置镜像;或反过来禁用 `ubuntu.sources`(改名 `ubuntu.sources.disabled`)后只维护 `sources.list`。 改完后执行 `sudo apt update`404/重复警告应明显减少。
6. **DEB822 示例amd64 用主镜像arm64 用 ubuntu-portsNoble**
将类似内容写入 `/etc/apt/sources.list.d/ubuntu.sources``Signed-By` 路径以本机为准;可与备份的 `ubuntu.sources.bak` 对照):
```text
Types: deb
URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Architectures: amd64
Types: deb
URIs: http://security.ubuntu.com/ubuntu
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Architectures: amd64
Types: deb
URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/
Suites: noble noble-updates noble-backports noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Architectures: arm64
```
然后:`sudo dpkg --add-architecture arm64`、`sudo apt update`,再执行 `apt-cache policy libc6:arm64`
### 无法在宿主机修好 multiarch 时的备选
**原生 arm64 环境**实体机、ARM 云主机、或 `arm64v8/ubuntu:24.04` 容器)里只跑 `npm run build:deb:arm64`(或 `tauri build … --target aarch64-unknown-linux-gnu`),可避免在 x86_64 上做整套 GTK/WebKit 交叉依赖。
并确保已添加 Rust 目标:
```bash
rustup target add aarch64-unknown-linux-gnu
```
安装完成后应存在:
`/usr/lib/aarch64-linux-gnu/pkgconfig/glib-2.0.pc`
构建脚本在开始 arm64 构建前会检查该文件;若缺失会直接退出并重复上述说明。
## 使用 Docker 打包Ubuntu 24.04.x 宿主机 + `build-linux-deb-all.sh`
**x86_64** 上装 Docker用镜像内已配好的 **Ubuntu 24.04 + amd64 主源 + ubuntu-portsarm64+ Node.jsUbuntu 官方仓库)+ Rust**,挂载你的仓库后执行 **`scripts/build-linux-deb-all.sh`**,产物仍在宿主机目录 **`dist/linux-deb/`** 与各项目 **`src-tauri/target/.../bundle/deb/`**。
### 1. 安装 Docker宿主机为 Ubuntu 24.04.4 LTS 示例)
```bash
sudo apt update
sudo apt install -y docker.io
sudo usermod -aG docker "$USER"
./scripts/docker/run-build.sh
```
注销重新登录后,`docker ps` 应不再要求 `sudo`
(若使用 Docker CE可按官方文档安装以下命令以 `docker.io` 为例。)
### 2. 进入仓库根目录(含 `call-client`、`broadcast-client`
只打指定项目:
```bash
cd /path/to/TauriClient
chmod +x scripts/docker/run-build.sh scripts/docker/container-entry.sh
./scripts/docker/run-build.sh call-client
./scripts/docker/run-build.sh broadcast-client
```
### 3. 一键:构建镜像并运行打包
可选模式:
```bash
./scripts/docker/run-build.sh
```
- 默认(双容器):`./scripts/docker/run-build.sh`
- 容器 #1 构建 `amd64`
- 容器 #2 构建 `arm64`
- 混合模式:`./scripts/docker/run-build.sh --hybrid`
- 宿主机构建 `amd64`
- 容器构建 `arm64`
该脚本会:
## 2. 打包产物位置
1. `docker build -f scripts/docker/Dockerfile -t tauri-linux-deb:24.04 .`(构建上下文为仓库根;`.dockerignore` 会排除 `node_modules`、`target` 以加快传输)
2. `docker run` 挂载当前仓库到容器内 **`/work`**,依次对 **`call-client`、`broadcast-client`** 执行 **`npm ci`**
3. 执行 **`/work/scripts/build-linux-deb-all.sh`**(与宿主机直接跑脚本等价)
统一产物目录:
只打部分项目(参数原样传给 `build-linux-deb-all.sh`
- `dist/linux-deb/call-client/amd64/*.deb`
- `dist/linux-deb/call-client/arm64/*.deb`
- `dist/linux-deb/broadcast-client/amd64/*.deb`
- `dist/linux-deb/broadcast-client/arm64/*.deb`
```bash
./scripts/docker/run-build.sh call-client
```
Tauri 原始输出目录:
自定义镜像名:
- `call-client/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/`
- `call-client/src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/`
- `broadcast-client/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/`
- `broadcast-client/src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/`
```bash
IMAGE_TAG=my-tauri-deb:24.04 ./scripts/docker/run-build.sh
```
## 3. 配置文件目录与日志目录
### 4. 仅构建镜像(不跑打包)
### 3.1 call-client
```bash
docker build -f scripts/docker/Dockerfile -t tauri-linux-deb:24.04 .
```
配置文件(运行时):
### 5. 手动 `docker run`(已与 `run-build.sh` 等价,便于改参数)
```bash
docker build -f scripts/docker/Dockerfile -t tauri-linux-deb:24.04 .
docker run --rm \
--platform linux/amd64 \
-v "$(pwd):/work" \
-w /work \
-e RUSTUP_HOME=/opt/rustup \
-e CARGO_HOME=/opt/cargo \
tauri-linux-deb:24.04 \
bash /work/scripts/docker/container-entry.sh
```
- Linux: `~/.config/call-client/config.json`
- Windows: `%APPDATA%/call-client/config.json`
### 6. 注意
日志输出(运行时):
- **宿主机须为 x86_64amd64**`run-build.sh` 使用 `--platform linux/amd64`,与脚本内先打 amd64 再交叉 arm64 一致。若在 **ARM 宿主机**上跑,需安装 QEMU/binfmt 且 **amd64 构建可能极慢或不可用**ARM 上更建议在 **`linux/arm64` 容器里只跑 `npm run build:deb:arm64`**。
- **网络**:镜像内 apt 使用 **Ubuntu官方** `archive.ubuntu.com` / `ports.ubuntu.com`;国内若慢,可自行改 **`scripts/docker/install-build-deps.sh`** 里 `ubuntu.sources` 的镜像地址后重新 `docker build`
- **新增子项目**:除改 **`build-linux-deb-all.sh``PROJECTS`**外,还要在 **`scripts/docker/container-entry.sh`** 的 `for d in ...` 中增加同名目录,否则容器内不会对该目录执行 `npm ci`
- Linux: `~/.local/state/call-client/app.log`
- Windows: `%LOCALAPPDATA%/call-client/state/app.log`
## 输出目录
### 3.2 broadcast-client
**集中拷贝(便于收取产物):**
配置存储(运行时):
- `dist/linux-deb/<项目名>/amd64/*.deb`
- `dist/linux-deb/<项目名>/arm64/*.deb`
- 当前使用浏览器存储(`localStorage`),无独立磁盘配置文件
- 关键键名:
- `runtime_broadcast_config`
- `broadcast_config_local_fallback`
**Tauri 默认目录(未改路径):**
日志输出运行时socket 服务):
- `<项目>/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/`
- `<项目>/src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/`
- Linux: `~/.config/broadcast-client/socket-service-*.log`
- Windows: `%APPDATA%/broadcast-client/socket-service-*.log`
## 新增同框架项目
### 3.3 两个项目仓库内 Tauri 配置文件
`scripts/build-linux-deb-all.sh``PROJECTS` 数组中增加一行目录名(与子项目目录名一致),并保证该项目的 `package.json` 中仍有 `build:deb:x64``build:deb:arm64`。若使用 **Docker** 打包,还要同步修改 **`scripts/docker/container-entry.sh`** 里的 `for d in ...` 列表。
- `call-client/src-tauri/tauri.conf.json`
- `broadcast-client/src-tauri/tauri.conf.json`

@ -8,14 +8,12 @@
"name": "broadcast-client",
"version": "0.1.0",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2",
"@tauri-apps/api": "^1",
"element-plus": "^2.11.4",
"vue": "^3.5.13"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@tauri-apps/cli": "^1",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"vite": "^6.0.3",
@ -922,21 +920,29 @@
]
},
"node_modules/@tauri-apps/api": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz",
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==",
"license": "Apache-2.0 OR MIT",
"engines": {
"node": ">= 14.6.0",
"npm": ">= 6.6.0",
"yarn": ">= 1.19.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
"integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.3.tgz",
"integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"dependencies": {
"semver": ">=7.5.2"
},
"bin": {
"tauri": "tauri.js"
},
@ -948,28 +954,27 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.10.1",
"@tauri-apps/cli-darwin-x64": "2.10.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
"@tauri-apps/cli-linux-arm64-musl": "2.10.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-musl": "2.10.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
"@tauri-apps/cli-win32-x64-msvc": "2.10.1"
"@tauri-apps/cli-darwin-arm64": "1.6.3",
"@tauri-apps/cli-darwin-x64": "1.6.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.3",
"@tauri-apps/cli-linux-arm64-musl": "1.6.3",
"@tauri-apps/cli-linux-x64-gnu": "1.6.3",
"@tauri-apps/cli-linux-x64-musl": "1.6.3",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.3",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.3",
"@tauri-apps/cli-win32-x64-msvc": "1.6.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
"integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -979,14 +984,14 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
"integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz",
"integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -996,14 +1001,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
"integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1013,14 +1018,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
"integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1030,31 +1035,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
"integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
"integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1064,14 +1052,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
"integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1081,14 +1069,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
"integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1098,14 +1086,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
"integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1115,14 +1103,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
"integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1132,14 +1120,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
"integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1148,24 +1136,6 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz",
"integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-store": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.2.tgz",
"integrity": "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@ -2044,6 +2014,19 @@
"fsevents": "~2.3.2"
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",

@ -21,14 +21,12 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2",
"@tauri-apps/api": "^1",
"element-plus": "^2.11.4",
"vue": "^3.5.13"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@tauri-apps/cli": "^1",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"vite": "^6.0.3",

File diff suppressed because it is too large Load Diff

@ -10,12 +10,10 @@ name = "broadcast_client_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-store = "2"
tauri = { version = "1", features = ["window-all"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["clock"] }

@ -14,7 +14,7 @@ use std::{
use chrono::Local;
use serde::{Deserialize, Serialize};
use tauri::{Emitter, Manager};
use tauri::Manager;
const SOCKET_PORT: u16 = 9501;
const SOCKET_STATUS_EVENT: &str = "socket-status";
@ -84,67 +84,20 @@ struct SocketCallEventPayload {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_store::Builder::default().build())
.manage(SocketServiceState::default())
.on_menu_event(|app, event| match event.id().as_ref() {
"open_sync_config" => {
let _ = ensure_config_window(app);
}
"minimize_main" => {
if let Some(main_window) = app.get_webview_window("main") {
let _ = main_window.minimize();
}
}
"quit_app" => app.exit(0),
_ => {}
})
.invoke_handler(tauri::generate_handler![
show_context_menu,
start_socket_service,
stop_socket_service,
get_socket_service_status
get_socket_service_status,
quit_app
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
/// 弹出同步屏窗口的右键菜单。
#[tauri::command]
fn show_context_menu(window: tauri::Window) -> Result<(), String> {
use tauri::menu::{Menu, MenuItem};
let config_item = MenuItem::with_id(
window.app_handle(),
"open_sync_config",
"配置同步屏窗口",
true,
None::<&str>,
)
.map_err(|error| format!("创建菜单项失败: {error}"))?;
let quit_item = MenuItem::with_id(
window.app_handle(),
"quit_app",
"退出",
true,
None::<&str>,
)
.map_err(|error| format!("创建菜单项失败: {error}"))?;
let minimize_item = MenuItem::with_id(
window.app_handle(),
"minimize_main",
"最小化",
true,
None::<&str>,
)
.map_err(|error| format!("创建菜单项失败: {error}"))?;
let menu = Menu::with_items(window.app_handle(), &[&config_item, &minimize_item, &quit_item])
.map_err(|error| format!("创建菜单失败: {error}"))?;
window
.popup_menu(&menu)
.map_err(|error| format!("弹出菜单失败: {error}"))
fn quit_app(app: tauri::AppHandle) {
app.exit(0);
}
/// 启动本地 socket 服务9501并推送服务状态事件。
@ -268,7 +221,7 @@ fn get_socket_service_status(state: tauri::State<'_, SocketServiceState>) -> Soc
}
fn emit_socket_status(app: &tauri::AppHandle, running: bool) {
let _ = app.emit(
let _ = app.emit_all(
SOCKET_STATUS_EVENT,
SocketStatusPayload {
running,
@ -368,7 +321,7 @@ fn try_dispatch_one_message(app: &tauri::AppHandle, text: &str) -> bool {
),
);
let _ = app.emit(
let _ = app.emit_all(
SOCKET_CALL_EVENT,
SocketCallEventPayload {
window_id,
@ -380,7 +333,7 @@ fn try_dispatch_one_message(app: &tauri::AppHandle, text: &str) -> bool {
}
fn append_socket_log<S: AsRef<str>>(app: &tauri::AppHandle, level: &str, message: S) {
let Ok(log_dir) = app.path().app_config_dir() else {
let Some(log_dir) = app.path_resolver().app_config_dir() else {
return;
};
@ -417,7 +370,7 @@ fn resolve_log_file_path(
let mut guard = state
.log_file
.lock()
.map_err(|_| tauri::Error::FailedToReceiveMessage)?;
.map_err(|_| tauri::Error::FailedToSendMessage)?;
if let Some(current) = guard.as_ref() {
let keep_current = std::fs::metadata(current)
@ -503,29 +456,3 @@ fn truncate_for_log(source: &str, max_chars: usize) -> String {
output
}
/// 确保配置窗口存在:
/// - 已存在则激活聚焦;
/// - 不存在则创建后显示。
fn ensure_config_window(app: &tauri::AppHandle) -> Result<(), String> {
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
if let Some(window) = app.get_webview_window("sync-config") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
return Ok(());
}
let window = WebviewWindowBuilder::new(app, "sync-config", WebviewUrl::App("/#/config".into()))
.title("配置同步屏窗口")
.inner_size(720.0, 460.0)
.resizable(true)
.decorations(true)
.always_on_top(true)
.build()
.map_err(|error| format!("创建配置窗口失败: {error}"))?;
let _ = window.show();
let _ = window.set_focus();
Ok(())
}

@ -1,15 +1,21 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "broadcast-client",
"version": "0.1.0",
"identifier": "com.ziyun.broadcastclient",
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "broadcast-client",
"version": "0.1.0"
},
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"app": {
"tauri": {
"allowlist": {
"window": {
"all": true
}
},
"windows": [
{
"label": "main",
@ -20,7 +26,6 @@
"y": 0,
"decorations": false,
"transparent": false,
"shadow": false,
"alwaysOnTop": true,
"resizable": false,
"maximizable": false,
@ -30,25 +35,24 @@
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["deb"],
"linux": {
},
"bundle": {
"active": true,
"targets": ["deb"],
"identifier": "com.ziyun.broadcastclient",
"deb": {
"depends": []
},
"appimage": {
"bundleMediaFramework": false
}
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}

@ -26,5 +26,8 @@ onUnmounted(() => {
window.removeEventListener("hashchange", syncHash);
});
const isConfigPage = computed(() => hash.value === "#/config");
const isConfigPage = computed(() => {
const params = new URLSearchParams(window.location.search);
return params.get("view") === "config" || hash.value === "#/config";
});
</script>

@ -2,8 +2,8 @@ import { onMounted, onUnmounted, ref, watch, type Ref } from "vue";
import {
LogicalSize,
PhysicalPosition,
appWindow,
currentMonitor,
getCurrentWindow,
} from "@tauri-apps/api/window";
import { buildSegments, normalizeScreenWidth } from "../services/segmentService";
import type { Segment } from "../models/ruler";
@ -44,7 +44,6 @@ export function useScreenInfo(
*/
async function syncWindowBounds() {
try {
const currentWindow = getCurrentWindow();
const segments = segmentsRef?.value?.length
? segmentsRef.value
: buildSegments(screenWidth.value, totalWidth.value, segmentHeight.value);
@ -57,8 +56,8 @@ export function useScreenInfo(
? Math.floor(configuredWindowHeight.value)
: calculatedHeight;
await currentWindow.setPosition(new PhysicalPosition(0, 0));
await currentWindow.setSize(new LogicalSize(screenWidth.value, targetHeight));
await appWindow.setPosition(new PhysicalPosition(0, 0));
await appWindow.setSize(new LogicalSize(screenWidth.value, targetHeight));
} catch {
// Tauri API 不可用时使用浏览器默认行为。
}

@ -1,5 +1,4 @@
import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
import { load, type Store } from "@tauri-apps/plugin-store";
import type {
BroadcastConfig,
ChildWindowAreaConfig,
@ -15,13 +14,10 @@ import {
normalizeTotalWidth,
} from "./segmentService";
const STORE_PATH = "broadcast-config.json";
const CONFIG_KEY = "runtime_broadcast_config";
const CONFIG_EVENT = "broadcast-config-updated";
const LOCAL_STORAGE_KEY = "broadcast_config_local_fallback";
let storePromise: Promise<Store> | null = null;
function normalizeFontSize(raw: unknown, fallback: number): number {
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : fallback;
}
@ -159,16 +155,6 @@ function normalizeConfig(raw: unknown): BroadcastConfig {
};
}
/**
* store
*/
async function getStore(): Promise<Store> {
if (storePromise === null) {
storePromise = load(STORE_PATH, { defaults: {}, autoSave: false });
}
return storePromise;
}
/**
* store 退
*/
@ -200,7 +186,7 @@ function setFallbackConfig(config: BroadcastConfig) {
*/
async function emitConfigUpdated(config: BroadcastConfig) {
try {
await emit<BroadcastConfig>(CONFIG_EVENT, config);
await emit(CONFIG_EVENT, config);
} catch {
// 浏览器模式下无宿主事件总线,忽略即可。
}
@ -211,12 +197,11 @@ async function emitConfigUpdated(config: BroadcastConfig) {
*/
export async function loadBroadcastConfig(): Promise<BroadcastConfig> {
try {
const store = await getStore();
const saved = await store.get<BroadcastConfig>(CONFIG_KEY);
const raw = window.localStorage.getItem(CONFIG_KEY);
const saved = raw ? (JSON.parse(raw) as BroadcastConfig) : null;
const config = saved ? normalizeConfig(saved) : DEFAULT_BROADCAST_CONFIG;
if (!saved) {
await store.set(CONFIG_KEY, config);
await store.save();
window.localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
}
return config;
} catch {
@ -230,9 +215,7 @@ export async function loadBroadcastConfig(): Promise<BroadcastConfig> {
export async function saveBroadcastConfig(payload: BroadcastConfig): Promise<BroadcastConfig> {
const config = normalizeConfig(payload);
try {
const store = await getStore();
await store.set(CONFIG_KEY, config);
await store.save();
window.localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
} catch {
setFallbackConfig(config);
}

@ -2,7 +2,7 @@
<main
class="broadcast-root"
:style="{ width: `${screenWidth}px`, height: `${containerHeight}px` }"
@contextmenu.prevent="showWindowMenu"
@dblclick.capture="openConfigWindow"
>
<RulerSegment
v-for="segment in segments"
@ -19,8 +19,8 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { WebviewWindow } from "@tauri-apps/api/window";
import RulerSegment from "../components/RulerSegment.vue";
import { useBroadcastConfig } from "../composables/useBroadcastConfig";
import { useRulerTicks } from "../composables/useRulerTicks";
@ -74,13 +74,30 @@ const subtitleAreaSlices = computed(() =>
);
/**
* 右键触发原生菜单配置窗口 / 退出
* 双击主屏直接打开配置窗口
*/
async function showWindowMenu() {
async function openConfigWindow() {
try {
await invoke("show_context_menu");
} catch {
//
const existing = WebviewWindow.getByLabel("sync-config");
if (existing) {
await existing.show();
await existing.unminimize();
await existing.setFocus();
return;
}
const configWindow = new WebviewWindow("sync-config", {
url: "/#/config",
title: "配置同步屏窗口",
width: 720,
height: 460,
resizable: true,
decorations: true,
alwaysOnTop: true,
});
void configWindow;
} catch (error) {
console.error("打开配置窗口失败:", error);
}
}

@ -308,6 +308,8 @@
<el-button type="danger" plain :disabled="!socketRunning" @click="stopSocketService">
停止 Socket 服务
</el-button>
<el-button @click="closeConfigWindow"></el-button>
<el-button type="danger" @click="quitApplication">退</el-button>
<span class="save-hint">{{ saveMessage }}</span>
</div>
</main>
@ -315,8 +317,8 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { currentMonitor } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/core";
import { appWindow, currentMonitor } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/tauri";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import type {
BroadcastConfig,
@ -514,6 +516,22 @@ async function stopSocketService() {
}
}
async function quitApplication() {
try {
await invoke("quit_app");
} catch (error) {
saveMessage.value = `退出失败: ${String(error)}`;
}
}
async function closeConfigWindow() {
try {
await appWindow.close();
} catch (error) {
saveMessage.value = `关闭配置窗口失败: ${String(error)}`;
}
}
/**
* 手动保存配置到持久化存储并广播给同步屏窗口实时生效
*/

@ -9,10 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/api": "^1",
"axios": "^1.14.0",
"element-plus": "^2.13.6",
"sass": "^1.98.0",
@ -22,7 +19,7 @@
"vue-router": "^5.0.4"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@tauri-apps/cli": "^1",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"vite": "^6.0.3",
@ -1279,21 +1276,29 @@
]
},
"node_modules/@tauri-apps/api": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz",
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==",
"license": "Apache-2.0 OR MIT",
"engines": {
"node": ">= 14.6.0",
"npm": ">= 6.6.0",
"yarn": ">= 1.19.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
"integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.6.3.tgz",
"integrity": "sha512-q46umd6QLRKDd4Gg6WyZBGa2fWvk0pbeUA5vFomm4uOs1/17LIciHv2iQ4UD+2Yv5H7AO8YiE1t50V0POiEGEw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"dependencies": {
"semver": ">=7.5.2"
},
"bin": {
"tauri": "tauri.js"
},
@ -1305,28 +1310,27 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.10.1",
"@tauri-apps/cli-darwin-x64": "2.10.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
"@tauri-apps/cli-linux-arm64-musl": "2.10.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-musl": "2.10.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
"@tauri-apps/cli-win32-x64-msvc": "2.10.1"
"@tauri-apps/cli-darwin-arm64": "1.6.3",
"@tauri-apps/cli-darwin-x64": "1.6.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "1.6.3",
"@tauri-apps/cli-linux-arm64-gnu": "1.6.3",
"@tauri-apps/cli-linux-arm64-musl": "1.6.3",
"@tauri-apps/cli-linux-x64-gnu": "1.6.3",
"@tauri-apps/cli-linux-x64-musl": "1.6.3",
"@tauri-apps/cli-win32-arm64-msvc": "1.6.3",
"@tauri-apps/cli-win32-ia32-msvc": "1.6.3",
"@tauri-apps/cli-win32-x64-msvc": "1.6.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
"integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -1336,14 +1340,14 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
"integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.6.3.tgz",
"integrity": "sha512-1yTXZzLajKAYINJOJhZfmMhCzweHSgKQ3bEgJSn6t+1vFkOgY8Yx4oFgWcybrrWI5J1ZLZAl47+LPOY81dLcyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -1353,14 +1357,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
"integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-CjTEr9r9xgjcvos09AQw8QMRPuH152B1jvlZt4PfAsyJNPFigzuwed5/SF7XAd8bFikA7zArP4UT12RdBxrx7w==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1370,14 +1374,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
"integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-G9EUUS4M8M/Jz1UKZqvJmQQCKOzgTb8/0jZKvfBuGfh5AjFBu8LHvlFpwkKVm1l4951Xg4ulUp6P9Q7WRJ9XSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1387,31 +1391,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
"integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-MuBTHJyNpZRbPVG8IZBN8+Zs7aKqwD22tkWVBcL1yOGL4zNNTJlkfL+zs5qxRnHlUsn6YAlbW/5HKocfpxVwBw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
"integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1421,14 +1408,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
"integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-Uvi7M+NK3tAjCZEY1WGel+dFlzJmqcvu3KND+nqa22762NFmOuBIZ4KJR/IQHfpEYqKFNUhJfCGnpUDfiC3Oxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1438,14 +1425,14 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
"integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-rc6B342C0ra8VezB/OJom9j/N+9oW4VRA4qMxS2f4bHY2B/z3J9NPOe6GOILeg4v/CV62ojkLsC3/K/CeF3fqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1455,14 +1442,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
"integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-cSH2qOBYuYC4UVIFtrc1YsGfc5tfYrotoHrpTvRjUGu0VywvmyNk82+ZsHEnWZ2UHmu3l3lXIGRqSWveLln0xg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1472,14 +1459,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
"integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-T8V6SJQqE4PSWmYBl0ChQVmS6AR2hXFHURH2DwAhgSGSQ6uBXgwlYFcfIeQpBQA727K2Eq8X2hGfvmoySyHMRw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1489,14 +1476,14 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
"integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-HUkWZ+lYHI/Gjkh2QjHD/OBDpqLVmvjZGpLK9losur1Eg974Jip6k+vsoTUxQBCBDfj30eDBct9E1FvXOspWeg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1505,33 +1492,6 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz",
"integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz",
"integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-store": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.2.tgz",
"integrity": "sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1892,9 +1852,9 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
@ -2193,9 +2153,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@ -2433,15 +2393,15 @@
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash-unified": {
@ -2820,6 +2780,19 @@
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -3036,9 +3009,9 @@
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true,
"license": "MIT",
"dependencies": {

@ -22,10 +22,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/api": "^1",
"axios": "^1.14.0",
"element-plus": "^2.13.6",
"sass": "^1.98.0",
@ -35,7 +32,7 @@
"vue-router": "^5.0.4"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@tauri-apps/cli": "^1",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
"vite": "^6.0.3",

File diff suppressed because it is too large Load Diff

@ -15,13 +15,10 @@ name = "call_client_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-opener = "2"
tauri = { version = "1", features = ["api-all"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-store = "2.4.2"

@ -1,5 +1,5 @@
use serde_json::Value;
use tauri::{AppHandle, Emitter, Manager};
use tauri::{AppHandle, Manager};
#[tauri::command]
pub fn emit_to_window(
@ -8,11 +8,15 @@ pub fn emit_to_window(
event: String,
payload: Value,
) -> Result<(), String> {
app.emit_to(label, event.as_str(), payload)
let Some(window) = app.get_window(label.as_str()) else {
return Err(format!("窗口不存在: {label}"));
};
window
.emit(event.as_str(), payload)
.map_err(|error| format!("发送窗口事件失败: {error}"))
}
#[tauri::command]
pub fn list_windows(app: AppHandle) -> Vec<String> {
app.webview_windows().keys().cloned().collect()
app.windows().keys().cloned().collect()
}

@ -1,11 +1,11 @@
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
use tauri::{AppHandle, Manager, WindowBuilder, WindowUrl};
pub fn ensure_main_window(app: &AppHandle) -> Result<(), String> {
if app.get_webview_window("main").is_some() {
pub fn ensure_main_window(app: AppHandle) -> Result<(), String> {
if app.get_window("main").is_some() {
return Ok(());
}
WebviewWindowBuilder::new(app, "main", WebviewUrl::App("/#/main".into()))
WindowBuilder::new(&app, "main", WindowUrl::App("/#/main".into()))
.title("Call Client")
.inner_size(500.0, 100.0)
.resizable(false)
@ -18,12 +18,12 @@ pub fn ensure_main_window(app: &AppHandle) -> Result<(), String> {
Ok(())
}
pub fn ensure_login_window(app: &AppHandle) -> Result<(), String> {
if app.get_webview_window("login").is_some() {
pub fn ensure_login_window(app: AppHandle) -> Result<(), String> {
if app.get_window("login").is_some() {
return Ok(());
}
WebviewWindowBuilder::new(app, "login", WebviewUrl::App("/#/login".into()))
WindowBuilder::new(&app, "login", WindowUrl::App("/#/login".into()))
.title("登录")
.inner_size(480.0, 600.0)
.resizable(false)
@ -37,7 +37,7 @@ pub fn ensure_login_window(app: &AppHandle) -> Result<(), String> {
#[tauri::command]
pub fn open_ticket_window(app: AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("ticketList") {
if let Some(window) = app.get_window("ticketList") {
// 优先复用已有窗口,避免频繁 close/recreate 引起的白屏状态。
let _ = window.eval(
"if (window.location.hash !== '#/ticketList') { window.location.hash = '/ticketList'; }",
@ -48,10 +48,10 @@ pub fn open_ticket_window(app: AppHandle) -> Result<(), String> {
return Ok(());
}
let builder = WebviewWindowBuilder::new(
let builder = WindowBuilder::new(
&app,
"ticketList",
WebviewUrl::App("/#/ticketList".into()),
WindowUrl::App("/#/ticketList".into()),
)
.title("票号列表")
.inner_size(1024.0, 720.0)
@ -75,7 +75,7 @@ pub fn open_ticket_window(app: AppHandle) -> Result<(), String> {
#[tauri::command]
pub fn close_ticket_window(app: AppHandle) -> Result<(), String> {
let Some(window) = app.get_webview_window("ticketList") else {
let Some(window) = app.get_window("ticketList") else {
return Ok(());
};
@ -86,7 +86,7 @@ pub fn close_ticket_window(app: AppHandle) -> Result<(), String> {
#[tauri::command]
pub fn focus_window(app: AppHandle, label: String) -> Result<(), String> {
let Some(window) = app.get_webview_window(label.as_str()) else {
let Some(window) = app.get_window(label.as_str()) else {
return Err(format!("窗口不存在: {label}"));
};
@ -98,9 +98,9 @@ pub fn focus_window(app: AppHandle, label: String) -> Result<(), String> {
#[tauri::command]
pub fn open_main_window(app: AppHandle) -> Result<(), String> {
ensure_main_window(&app)?;
ensure_main_window(app.clone())?;
let Some(main_window) = app.get_webview_window("main") else {
let Some(main_window) = app.get_window("main") else {
return Err("主窗口不存在".to_string());
};
@ -108,7 +108,7 @@ pub fn open_main_window(app: AppHandle) -> Result<(), String> {
let _ = main_window.unminimize();
let _ = main_window.set_focus();
if let Some(login_window) = app.get_webview_window("login") {
if let Some(login_window) = app.get_window("login") {
let _ = login_window.close();
}
@ -117,9 +117,9 @@ pub fn open_main_window(app: AppHandle) -> Result<(), String> {
#[tauri::command]
pub fn open_login_window(app: AppHandle) -> Result<(), String> {
ensure_login_window(&app)?;
ensure_login_window(app.clone())?;
let Some(login_window) = app.get_webview_window("login") else {
let Some(login_window) = app.get_window("login") else {
return Err("登录窗口不存在".to_string());
};
@ -127,7 +127,7 @@ pub fn open_login_window(app: AppHandle) -> Result<(), String> {
let _ = login_window.unminimize();
let _ = login_window.set_focus();
if let Some(main_window) = app.get_webview_window("main") {
if let Some(main_window) = app.get_window("main") {
let _ = main_window.hide();
}

@ -16,9 +16,6 @@ use state::AppState;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_store::Builder::default().build())
.manage(AppState::default())
.setup(|app| {
ensure_main_window(app.handle())?;

@ -1,15 +1,19 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "call-client",
"version": "0.1.0",
"identifier": "com.ziyun.callclient",
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "call-client",
"version": "0.1.0"
},
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"app": {
"tauri": {
"allowlist": {
"all": true
},
"windows": [
{
"label": "login",
@ -37,25 +41,24 @@
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["deb"],
"linux": {
},
"bundle": {
"active": true,
"targets": ["deb"],
"identifier": "com.ziyun.callclient",
"deb": {
"depends": []
},
"appimage": {
"bundleMediaFramework": false
}
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}

@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/tauri";
import type { AppConfig } from "./types";
/**

@ -1,4 +1,4 @@
import { confirm, message } from "@tauri-apps/plugin-dialog";
import { ask, message } from "@tauri-apps/api/dialog";
import type { NativeConfirmOptions } from "./types";
/**
@ -6,7 +6,7 @@ import type { NativeConfirmOptions } from "./types";
*/
export async function confirmNative(options: NativeConfirmOptions): Promise<boolean> {
try {
return await confirm(options.message, {
return await ask(options.message, {
title: options.title,
okLabel: options.okLabel,
cancelLabel: options.cancelLabel,
@ -21,7 +21,7 @@ export async function confirmNative(options: NativeConfirmOptions): Promise<bool
*/
export async function showErrorNative(content: string, title = "错误"): Promise<void> {
try {
await message(content, { title, kind: "error" });
await message(content, { title });
} catch (error) {
throw new Error(`打开错误提示框失败: ${String(error)}`);
}

@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/tauri";
import type { LogLevel } from "./types";
/**

@ -1,7 +1,5 @@
import { load, type Store } from "@tauri-apps/plugin-store";
import type { SessionState } from "./types";
const STORE_PATH = "global-session.json";
const SESSION_KEY = "runtime_session";
const DEFAULT_SESSION: SessionState = {
empUid: null,
@ -9,15 +7,6 @@ const DEFAULT_SESSION: SessionState = {
queueToken: null,
};
let storePromise: Promise<Store> | null = null;
async function getStore(): Promise<Store> {
if (storePromise === null) {
storePromise = load(STORE_PATH, { defaults: {}, autoSave: false });
}
return storePromise;
}
function normalizeSession(raw: unknown): SessionState {
const source = (raw ?? {}) as Partial<SessionState>;
@ -31,30 +20,27 @@ function normalizeSession(raw: unknown): SessionState {
}
/**
* Store
* localStorage
*/
export async function getSession(): Promise<SessionState> {
try {
const store = await getStore();
const value = await store.get<SessionState>(SESSION_KEY);
if (value === undefined) {
const value = window.localStorage.getItem(SESSION_KEY);
if (!value) {
return { ...DEFAULT_SESSION };
}
return normalizeSession(value);
return normalizeSession(JSON.parse(value));
} catch (error) {
throw new Error(`读取会话失败: ${String(error)}`);
}
}
/**
* Store
* localStorage
*/
export async function setSession(payload: SessionState): Promise<SessionState> {
try {
const normalized = normalizeSession(payload);
const store = await getStore();
await store.set(SESSION_KEY, normalized);
await store.save();
window.localStorage.setItem(SESSION_KEY, JSON.stringify(normalized));
return normalized;
} catch (error) {
throw new Error(`写入会话失败: ${String(error)}`);
@ -62,13 +48,11 @@ export async function setSession(payload: SessionState): Promise<SessionState> {
}
/**
* Store
* localStorage
*/
export async function clearSession(): Promise<SessionState> {
try {
const store = await getStore();
await store.set(SESSION_KEY, DEFAULT_SESSION);
await store.save();
window.localStorage.setItem(SESSION_KEY, JSON.stringify(DEFAULT_SESSION));
return { ...DEFAULT_SESSION };
} catch (error) {
throw new Error(`清空会话失败: ${String(error)}`);

@ -1,12 +1,12 @@
import { invoke } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { invoke } from "@tauri-apps/api/tauri";
import { appWindow } from "@tauri-apps/api/window";
/**
*
*/
export async function minimizeWindow(): Promise<void> {
try {
await getCurrentWindow().minimize();
await appWindow.minimize();
} catch (error) {
throw new Error(`最小化窗口失败: ${String(error)}`);
}
@ -17,7 +17,7 @@ export async function minimizeWindow(): Promise<void> {
*/
export async function closeWindow(): Promise<void> {
try {
await getCurrentWindow().close();
await appWindow.close();
} catch (error) {
throw new Error(`关闭窗口失败: ${String(error)}`);
}
@ -28,7 +28,7 @@ export async function closeWindow(): Promise<void> {
*/
export async function startWindowDragging(): Promise<void> {
try {
await getCurrentWindow().startDragging();
await appWindow.startDragging();
} catch (error) {
throw new Error(`拖拽窗口失败: ${String(error)}`);
}

@ -11,6 +11,7 @@ import {
closeWindow,
minimizeWindow,
openMainWindow,
startWindowDragging,
} from "../host/window";
const username = ref("admin");
@ -199,6 +200,17 @@ async function handleCloseClick(event: MouseEvent): Promise<void> {
}
}
async function handleDragMouseDown(event: MouseEvent): Promise<void> {
if (event.button !== 0) {
return;
}
try {
await startWindowDragging();
} catch {
// 退 data-tauri-drag-region
}
}
/**
* 处理回车键触发登录
*/
@ -244,7 +256,12 @@ onUnmounted(() => {
</div>
<div class="login-header">
<div class="login-header-drag" data-tauri-drag-region @dblclick.prevent.stop></div>
<div
class="login-header-drag"
data-tauri-drag-region
@dblclick.prevent.stop
@mousedown="handleDragMouseDown"
></div>
<div class="login-header-actions">
<button
class="control-button"
@ -271,7 +288,12 @@ onUnmounted(() => {
</div>
<div class="login-main">
<div class="header-section" data-tauri-drag-region @dblclick.prevent.stop>
<div
class="header-section"
data-tauri-drag-region
@dblclick.prevent.stop
@mousedown="handleDragMouseDown"
>
<div class="app-info" data-tauri-drag-region>
<h1 class="app-title" data-tauri-drag-region>紫云智慧大厅</h1>
<h2 class="app-subtitle" data-tauri-drag-region>呼叫客户端系统</h2>

@ -1,27 +1,36 @@
<script setup lang="ts">
import {
Back,
CircleCheck,
Close,
Delete,
ForkSpoon,
HotWater,
Memo,
Menu as MenuIcon,
MessageBox,
MoreFilled,
Phone,
Star,
Switch,
User,
VideoPause,
VideoPlay,
} from "@element-plus/icons-vue";
import { LogicalPosition } from "@tauri-apps/api/dpi";
import { Menu } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { appWindow } from "@tauri-apps/api/window";
import { ElMessage } from "element-plus";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { api } from "../api";
import { confirmNative } from "../host/dialog";
import { showErrorNative } from "../host/dialog";
import { confirmNative, showErrorNative } from "../host/dialog";
import { listenMainAction } from "../host/events";
import { log } from "../host/logger";
import { getSession } from "../host/session";
import type { SessionState } from "../host/types";
import { minimizeWindow, openTicketListWindow, quitApplication } from "../host/window";
import {
minimizeWindow,
openTicketListWindow,
quitApplication,
} from "../host/window";
import type { ActionButton } from "../types/action";
type ActionResultLike = {
@ -56,61 +65,11 @@ let isRankPollingBusy = false;
let queueCountPollingTimer: ReturnType<typeof setInterval> | null = null;
let unlistenWindowFocusChanged: (() => void) | null = null;
let unlistenMainAction: (() => void) | null = null;
let moreNativeMenuPromise: Promise<Menu> | null = null;
let pauseNativeMenuPromise: Promise<Menu> | null = null;
const EVALUATING_COUNTDOWN_SEC = 15;
const pauseReasonOptions = ["午休", "休息一下", "整理资料", "其他"];
const isMainWindowActive = ref(false);
function getMoreNativeMenu(): Promise<Menu> {
if (moreNativeMenuPromise === null) {
moreNativeMenuPromise = Menu.new({
items: [
{
id: "main",
text: "办税员窗口",
action: () => {
void handleMoreCommand("main");
},
},
{
id: "ticketList",
text: "票号列表",
action: () => {
void handleMoreCommand("ticketList");
},
},
{ item: "Separator" },
{
id: "logout",
text: "退出程序",
action: () => {
void handleMoreCommand("logout");
},
},
],
});
}
return moreNativeMenuPromise;
}
function getPauseNativeMenu(): Promise<Menu> {
if (pauseNativeMenuPromise === null) {
pauseNativeMenuPromise = Menu.new({
items: pauseReasonOptions.map((reason) => ({
id: reason,
text: reason,
action: () => {
void confirmPauseReason(reason);
},
})),
});
}
return pauseNativeMenuPromise;
}
const buttonPanel = ref<"main" | "more" | "pause">("main");
/**
* 记录错误日志
@ -123,7 +82,9 @@ async function logErr(context: string, error: unknown): Promise<void> {
/**
* 提取后端动作返回对象
*/
function getActionData(res: unknown): NonNullable<ActionResultLike["data"]> | ActionResultLike {
function getActionData(
res: unknown,
): NonNullable<ActionResultLike["data"]> | ActionResultLike {
const result = (res ?? {}) as ActionResultLike;
return result.data && typeof result.data === "object" ? result.data : result;
}
@ -292,7 +253,10 @@ async function pollQueueCountOnce(): Promise<void> {
}
const res = await api.action.getQueueCount({ windowUid });
const count = typeof res.queueCount === "number" ? res.queueCount : Number(res.count ?? 0);
const count =
typeof res.queueCount === "number"
? res.queueCount
: Number(res.count ?? 0);
message.value = `欢迎使用紫云呼叫终端,当前窗口等候人数:${count}`;
} catch (error) {
await logErr("查询 getQueueCount 失败", error);
@ -359,7 +323,13 @@ const buttons = computed<ActionButton[]>(() => {
icon: VideoPlay,
label: "开始",
action: "start",
enabled: !["idle", "paused", "working", "evaluating", "transferring"].includes(callStatus.value),
enabled: ![
"idle",
"paused",
"working",
"evaluating",
"transferring",
].includes(callStatus.value),
};
return [
@ -367,7 +337,9 @@ const buttons = computed<ActionButton[]>(() => {
icon: Phone,
label: callBtnText.value,
action: "call",
enabled: !["paused", "working", "evaluating", "transferring"].includes(callStatus.value),
enabled: !["paused", "working", "evaluating", "transferring"].includes(
callStatus.value,
),
},
startOrComplete,
{
@ -380,19 +352,33 @@ const buttons = computed<ActionButton[]>(() => {
icon: Switch,
label: "转移",
action: "transfer",
enabled: !["idle", "calling", "paused", "evaluating", "transferring"].includes(callStatus.value),
enabled: ![
"idle",
"calling",
"paused",
"evaluating",
"transferring",
].includes(callStatus.value),
},
{
icon: VideoPause,
label: pauseBtnText.value,
action: "pause",
enabled: !["calling", "working", "evaluating", "transferring"].includes(callStatus.value),
enabled: !["calling", "working", "evaluating", "transferring"].includes(
callStatus.value,
),
},
{
icon: Star,
label: "评价",
action: "evaluate",
enabled: !["idle", "calling", "paused", "working", "transferring"].includes(callStatus.value),
enabled: ![
"idle",
"calling",
"paused",
"working",
"transferring",
].includes(callStatus.value),
},
];
});
@ -414,7 +400,11 @@ async function callAction(): Promise<void> {
const ticketUid = callingTkt.value > 0 ? callingTkt.value : null;
if (callStatus.value === "calling" && callingTkt.value > 0) {
const recallRes = await api.action.recall({ windowUid, empUid, ticketUid: callingTkt.value });
const recallRes = await api.action.recall({
windowUid,
empUid,
ticketUid: callingTkt.value,
});
if (isActionSuccess(recallRes)) {
updateLog(`已重呼:${getActionTicketNo(recallRes)},请勿重复点击!`);
await log("info", `重呼成功: ticketNo=${getActionTicketNo(recallRes)}`);
@ -430,7 +420,10 @@ async function callAction(): Promise<void> {
callBtnText.value = "重呼";
callingTkt.value = getActionTicketUid(res);
updateLog(`正在呼叫:${getActionTicketNo(res)}`);
await log("info", `呼叫成功: ticketNo=${getActionTicketNo(res)}, ticketUid=${getActionTicketUid(res)}`);
await log(
"info",
`呼叫成功: ticketNo=${getActionTicketNo(res)}, ticketUid=${getActionTicketUid(res)}`,
);
return;
}
@ -554,7 +547,7 @@ async function evaluateAction(): Promise<void> {
async function pauseAction(): Promise<void> {
try {
if (callStatus.value !== "paused") {
await openPauseContextMenu();
buttonPanel.value = "pause";
return;
}
@ -594,12 +587,15 @@ async function confirmPauseReason(reason: string): Promise<void> {
callStatus.value = "paused";
pauseBtnText.value = "恢复";
updateLog(`暂停中,原因:${pauseReason}`);
buttonPanel.value = "main";
return;
}
updateLog(`暂停未成功: ${getActionMessage(res) || "unknown"}`);
} catch (error) {
await logErr("暂停失败", error);
} finally {
buttonPanel.value = "main";
}
}
@ -638,43 +634,39 @@ function handleButtonClick(button: ActionButton): void {
}
}
async function openMoreContextMenu(event: MouseEvent): Promise<void> {
event.preventDefault();
event.stopPropagation();
const position = new LogicalPosition(event.clientX, event.clientY + 8);
try {
const menu = await getMoreNativeMenu();
await menu.popup(position);
} catch (error) {
await logErr("更多菜单 popup 失败", error);
await showErrorNative("更多菜单弹出失败,请查看日志");
}
async function openMoreContextMenu(): Promise<void> {
buttonPanel.value = "more";
}
async function openPauseContextMenu(): Promise<void> {
try {
const menu = await getPauseNativeMenu();
const pauseButton = document.querySelector<HTMLButtonElement>('[data-action="pause"]');
if (!pauseButton) {
await menu.popup(new LogicalPosition(16, 56));
return;
}
function backToMainPanel(): void {
buttonPanel.value = "main";
}
const rect = pauseButton.getBoundingClientRect();
await menu.popup(new LogicalPosition(rect.left, rect.bottom + 8));
} catch (error) {
await logErr("暂停菜单 popup 失败", error);
await showErrorNative("暂停菜单弹出失败,请查看日志");
function getPauseReasonIcon(reason: string) {
switch (reason) {
case "午休":
return ForkSpoon;
case "休息一下":
return HotWater;
case "整理资料":
return MessageBox;
case "其他":
return MoreFilled;
default:
return null;
}
}
/**
* 处理顶部更多菜单动作
*/
async function handleMoreCommand(command: "main" | "ticketList" | "logout"): Promise<void> {
async function handleMoreCommand(
command: "main" | "ticketList" | "logout",
): Promise<void> {
try {
if (command === "main") {
updateLog("当前已在办税员窗口");
buttonPanel.value = "main";
return;
}
@ -684,6 +676,7 @@ async function handleMoreCommand(command: "main" | "ticketList" | "logout"): Pro
await minimizeWindow();
await openTicketList();
updateLog("票号列表已打开");
buttonPanel.value = "main";
return;
}
@ -703,6 +696,10 @@ async function handleMoreCommand(command: "main" | "ticketList" | "logout"): Pro
await logErr(`更多菜单处理失败: ${command}`, error);
await showErrorNative("操作失败,请查看日志");
updateLog(`操作失败: ${command}`);
} finally {
if (command !== "logout") {
buttonPanel.value = "main";
}
}
}
@ -713,7 +710,8 @@ onMounted(async () => {
empUid: parseOptionalNumber(session.empUid),
winUid: parseOptionalNumber(session.winUid),
queueToken:
typeof session.queueToken === "string" && session.queueToken.trim() !== ""
typeof session.queueToken === "string" &&
session.queueToken.trim() !== ""
? session.queueToken
: null,
};
@ -722,11 +720,19 @@ onMounted(async () => {
}
try {
const currentWindow = getCurrentWindow();
isMainWindowActive.value = await currentWindow.isVisible();
unlistenWindowFocusChanged = await currentWindow.onFocusChanged(({ payload }) => {
isMainWindowActive.value = payload;
});
isMainWindowActive.value = await appWindow.isVisible();
const onFocus = () => {
isMainWindowActive.value = true;
};
const onBlur = () => {
isMainWindowActive.value = false;
};
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
unlistenWindowFocusChanged = () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
} catch (error) {
await logErr("订阅主窗口焦点事件失败", error);
}
@ -762,27 +768,104 @@ onUnmounted(() => {
<template>
<div class="main-bg" @dblclick.prevent.stop>
<div class="btn-div">
<button class="action-button action-button-menu" type="button" @click="openMoreContextMenu">
<el-icon class="button-icon">
<component :is="MenuIcon" />
</el-icon>
</button>
<div class="divider-vertical"></div>
<button
v-for="(btn, index) in buttons"
:key="index"
type="button"
class="action-button"
:data-action="btn.action"
:class="{ disabled: !btn.enabled }"
:style="{ color: !btn.enabled ? '#ccc' : textColor }"
@click="handleButtonClick(btn)"
>
<el-icon class="button-icon" :style="{ color: !btn.enabled ? '#ccc' : iconColor }">
<component :is="btn.icon" />
</el-icon>
<span class="button-label">{{ btn.label }}</span>
</button>
<template v-if="buttonPanel === 'main'">
<button
class="action-button action-button-menu"
type="button"
@click="openMoreContextMenu"
>
<el-icon class="button-icon">
<component :is="MenuIcon" />
</el-icon>
</button>
<div class="divider-vertical"></div>
<button
v-for="(btn, index) in buttons"
:key="index"
type="button"
class="action-button"
:data-action="btn.action"
:class="{ disabled: !btn.enabled }"
:style="{ color: !btn.enabled ? '#ccc' : textColor }"
@click="handleButtonClick(btn)"
>
<el-icon
class="button-icon"
:style="{ color: !btn.enabled ? '#ccc' : iconColor }"
>
<component :is="btn.icon" />
</el-icon>
<span class="button-label">{{ btn.label }}</span>
</button>
</template>
<template v-else-if="buttonPanel === 'more'">
<button
class="action-button action-button-panel"
type="button"
@click="handleMoreCommand('main')"
>
<el-icon class="button-icon">
<component :is="User" />
</el-icon>
<span class="button-label">办税员窗口</span>
</button>
<button
class="action-button action-button-panel"
type="button"
@click="handleMoreCommand('ticketList')"
>
<el-icon class="button-icon">
<component :is="Memo" />
</el-icon>
<span class="button-label">票号列表</span>
</button>
<button
class="action-button action-button-panel"
type="button"
@click="handleMoreCommand('logout')"
>
<el-icon class="button-icon">
<component :is="Close" />
</el-icon>
<span class="button-label">退出程序</span>
</button>
<button
class="action-button action-button-panel action-button-back"
type="button"
@click="backToMainPanel"
>
<el-icon class="button-icon">
<component :is="Back" />
</el-icon>
<span class="button-label">返回</span>
</button>
</template>
<template v-else>
<button
v-for="reason in pauseReasonOptions"
:key="reason"
class="action-button action-button-panel"
type="button"
@click="confirmPauseReason(reason)"
>
<el-icon v-if="getPauseReasonIcon(reason)" class="button-icon">
<component :is="getPauseReasonIcon(reason)" />
</el-icon>
<span class="button-label">{{ reason }}</span>
</button>
<button
class="action-button action-button-panel action-button-back"
type="button"
@click="backToMainPanel"
>
<el-icon class="button-icon">
<component :is="Back" />
</el-icon>
<span class="button-label">返回</span>
</button>
</template>
</div>
<div class="divider-horizontal"></div>
<div class="log-div" data-tauri-drag-region @dblclick.prevent.stop>
@ -800,8 +883,11 @@ onUnmounted(() => {
flex-direction: column;
justify-content: center;
align-items: center;
background:
linear-gradient(180deg, rgba(53, 64, 94, 0.96) 0%, rgba(25, 29, 40, 0.98) 100%);
background: linear-gradient(
180deg,
rgba(53, 64, 94, 0.96) 0%,
rgba(25, 29, 40, 0.98) 100%
);
}
.btn-div {
@ -877,9 +963,12 @@ onUnmounted(() => {
color: #99ccff;
}
.action-button-panel {
color: #fff;
}
.action-button-close {
margin-left: auto;
color: #dcdfe6;
}
</style>

@ -102,7 +102,7 @@
<script setup lang="ts">
import { Close, Minus } from "@element-plus/icons-vue";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { appWindow } from "@tauri-apps/api/window";
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import { computed, onMounted, onUnmounted, ref } from "vue";
@ -443,17 +443,26 @@ onMounted(async () => {
console.info("[TicketListView] mounted");
await safeLog("info", "TicketListView mounted");
try {
const currentWindow = getCurrentWindow();
isTicketWindowActive.value = await currentWindow.isVisible();
unlistenWindowFocusChanged = await currentWindow.onFocusChanged(({ payload }) => {
isTicketWindowActive.value = payload;
if (payload) {
scheduleImmediateRefresh();
startRefreshPolling();
} else {
stopRefreshPolling();
}
});
isTicketWindowActive.value = await appWindow.isVisible();
const onFocus = () => {
isTicketWindowActive.value = true;
scheduleImmediateRefresh();
startRefreshPolling();
};
const onBlur = () => {
isTicketWindowActive.value = false;
stopRefreshPolling();
};
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
unlistenWindowFocusChanged = () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
if (isTicketWindowActive.value) {
scheduleImmediateRefresh();
startRefreshPolling();
}
const session = await getSession();
tableHeight.value = 720 - 220;

@ -1,10 +1,10 @@
#!/usr/bin/env bash
# 在 Ubuntu 24.04.xx86_64 宿主机)上为多个 Tauri 2 + Vue 项目打出 amd64 与 arm64 的 .deb。
# 在 Ubuntu 24.04.xx86_64 宿主机)上为多个 Tauri v1 + Vue 项目打出 amd64 与 arm64 的 .deb。
#
# 依赖:
# - Node/npm、Rust、@tauri-apps/cli
# - rustup target add aarch64-unknown-linux-gnu
# - 本机 amd64Tauri Linux 前置依赖libwebkit2gtk-4.1-dev 等)
# - 本机 amd64Tauri Linux 前置依赖libwebkit2gtk-4.0-dev 等)
# - 交叉 arm64必须启用 multiarch 并安装 *:arm64 的 -dev 包,否则 glib/gdk/webkit 等 -sys 会报找不到 .pc
# 见 README.md「交叉编译 arm64」或本脚本 check_arm64_pkgconfig 失败时的提示。
#
@ -111,7 +111,7 @@ parse_args() {
check_x64_pkgconfig() {
local missing=()
local f
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.1.pc; do
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.0.pc; do
if [[ ! -f "$X64_PKGCONFIG_DIR/$f" ]]; then
missing+=( "$f" )
fi
@ -127,11 +127,11 @@ check_x64_pkgconfig() {
请先安装本机 amd64 的开发包:
sudo apt install -y --no-install-recommends \\
libglib2.0-dev libgtk-3-dev \\
libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1-dev \\
libwebkit2gtk-4.0-dev libjavascriptcoregtk-4.0-dev \\
libayatana-appindicator3-dev librsvg2-dev libssl-dev
安装后自检:
ls $X64_PKGCONFIG_DIR/glib-2.0.pc $X64_PKGCONFIG_DIR/gdk-3.0.pc $X64_PKGCONFIG_DIR/webkit2gtk-4.1.pc
ls $X64_PKGCONFIG_DIR/glib-2.0.pc $X64_PKGCONFIG_DIR/gdk-3.0.pc $X64_PKGCONFIG_DIR/webkit2gtk-4.0.pc
EOF
exit 1
}
@ -140,7 +140,7 @@ check_arm64_pkgconfig() {
# 缺任一则 cargo 会在不同阶段失败gdk-3.0.pc 由 libgtk-3-dev:arm64 提供
local missing=()
local f
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.1.pc; do
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.0.pc; do
if [[ ! -f "$ARM64_PKGCONFIG_DIR/$f" ]]; then
missing+=( "$f" )
fi
@ -155,7 +155,7 @@ check_arm64_pkgconfig() {
- glib-2.0.pc → libglib2.0-dev:arm64
- gdk-3.0.pc → libgtk-3-dev:arm64gdk-sys
- webkit2gtk-4.1.pc → libwebkit2gtk-4.1-dev:arm64
- webkit2gtk-4.0.pc → libwebkit2gtk-4.0-dev:arm64
请确认已按 README 配置 ubuntu-portsarm64然后安装可整段执行
@ -165,13 +165,13 @@ check_arm64_pkgconfig() {
libgtk-3-dev:arm64 \\
libcairo2-dev:arm64 libpango1.0-dev:arm64 \\
libgdk-pixbuf-2.0-dev:arm64 libatk1.0-dev:arm64 libepoxy-dev:arm64 \\
libwebkit2gtk-4.1-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64 \\
libwebkit2gtk-4.0-dev:arm64 libjavascriptcoregtk-4.0-dev:arm64 \\
libssl-dev:arm64 libayatana-appindicator3-dev:arm64 \\
librsvg2-dev:arm64 patchelf
安装后自检:
ls $ARM64_PKGCONFIG_DIR/glib-2.0.pc $ARM64_PKGCONFIG_DIR/gdk-3.0.pc $ARM64_PKGCONFIG_DIR/webkit2gtk-4.1.pc
ls $ARM64_PKGCONFIG_DIR/glib-2.0.pc $ARM64_PKGCONFIG_DIR/gdk-3.0.pc $ARM64_PKGCONFIG_DIR/webkit2gtk-4.0.pc
详见 README.md「交叉编译 arm64」与「gdk-3.0 / gdk-sys」。
EOF

@ -15,7 +15,7 @@ npm config set fetch-timeout 300000
ensure_amd64_deps() {
local x64_pc_dir="/usr/lib/x86_64-linux-gnu/pkgconfig"
local missing=0
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.1.pc; do
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.0.pc; do
if [[ ! -f "$x64_pc_dir/$f" ]]; then
missing=1
break
@ -30,14 +30,14 @@ ensure_amd64_deps() {
apt-get install -y --no-install-recommends \
build-essential:amd64 pkg-config:amd64 patchelf:amd64 \
libglib2.0-dev:amd64 libgtk-3-dev:amd64 \
libwebkit2gtk-4.1-dev:amd64 libjavascriptcoregtk-4.1-dev:amd64 \
libwebkit2gtk-4.0-dev:amd64 libjavascriptcoregtk-4.0-dev:amd64 \
libayatana-appindicator3-dev:amd64 librsvg2-dev:amd64 libssl-dev:amd64
}
ensure_arm64_deps() {
local arm64_pc_dir="/usr/lib/aarch64-linux-gnu/pkgconfig"
local missing=0
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.1.pc; do
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.0.pc; do
if [[ ! -f "$arm64_pc_dir/$f" ]]; then
missing=1
break
@ -55,7 +55,7 @@ ensure_arm64_deps() {
libglib2.0-dev:arm64 libgtk-3-dev:arm64 \
libcairo2-dev:arm64 libpango1.0-dev:arm64 \
libgdk-pixbuf-2.0-dev:arm64 libatk1.0-dev:arm64 libepoxy-dev:arm64 \
libwebkit2gtk-4.1-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64 \
libwebkit2gtk-4.0-dev:arm64 libjavascriptcoregtk-4.0-dev:arm64 \
libssl-dev:arm64 libayatana-appindicator3-dev:arm64 librsvg2-dev:arm64
}

@ -40,14 +40,14 @@ apt-get install -y --no-install-recommends \
apt-get install -y --no-install-recommends \
build-essential pkg-config patchelf \
libglib2.0-dev libgtk-3-dev \
libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1-dev \
libwebkit2gtk-4.0-dev libjavascriptcoregtk-4.0-dev \
libayatana-appindicator3-dev librsvg2-dev libssl-dev
# 构建阶段自检:避免缓存/半装镜像拖到 run 时才在 build-linux-deb-all.sh 里失败
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.1.pc; do
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.0.pc; do
if [[ ! -f "/usr/lib/x86_64-linux-gnu/pkgconfig/${f}" ]]; then
echo "ERROR: amd64 缺少 pkg-config: /usr/lib/x86_64-linux-gnu/pkgconfig/${f}" >&2
echo "请检查 libwebkit2gtk-4.1-dev 等是否已正确安装。" >&2
echo "请检查 libwebkit2gtk-4.0-dev 等是否已正确安装。" >&2
exit 1
fi
done
@ -59,7 +59,7 @@ if ! apt-get install -y --no-install-recommends \
libglib2.0-dev:arm64 libgtk-3-dev:arm64 \
libcairo2-dev:arm64 libpango1.0-dev:arm64 \
libgdk-pixbuf-2.0-dev:arm64 libatk1.0-dev:arm64 libepoxy-dev:arm64 \
libwebkit2gtk-4.1-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64 \
libwebkit2gtk-4.0-dev:arm64 libjavascriptcoregtk-4.0-dev:arm64 \
libssl-dev:arm64 libayatana-appindicator3-dev:arm64 librsvg2-dev:arm64; then
apt-get -f install -y
apt-get install -y --no-install-recommends \
@ -68,11 +68,11 @@ if ! apt-get install -y --no-install-recommends \
libglib2.0-dev:arm64 libgtk-3-dev:arm64 \
libcairo2-dev:arm64 libpango1.0-dev:arm64 \
libgdk-pixbuf-2.0-dev:arm64 libatk1.0-dev:arm64 libepoxy-dev:arm64 \
libwebkit2gtk-4.1-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64 \
libwebkit2gtk-4.0-dev:arm64 libjavascriptcoregtk-4.0-dev:arm64 \
libssl-dev:arm64 libayatana-appindicator3-dev:arm64 librsvg2-dev:arm64
fi
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.1.pc; do
for f in glib-2.0.pc gdk-3.0.pc webkit2gtk-4.0.pc; do
if [[ ! -f "/usr/lib/aarch64-linux-gnu/pkgconfig/${f}" ]]; then
echo "ERROR: arm64 缺少 pkg-config: /usr/lib/aarch64-linux-gnu/pkgconfig/${f}" >&2
exit 1

@ -1,11 +1,11 @@
#!/usr/bin/env bash
# 在宿主机上:先打 amd64再在容器中打 arm64均支持自动发现项目
# 双容器打包:分别在容器内构建 amd64 与 arm64均支持自动发现项目
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IMAGE_TAG="${IMAGE_TAG:-tauri-linux-deb:24.04}"
RUN_MODE="${RUN_MODE:-hybrid}" # hybrid: host amd64 + docker arm64; docker: docker all
RUN_MODE="${RUN_MODE:-docker}" # docker: dual-container all; hybrid: host amd64 + docker arm64
PROJECT_ARGS=()
while (($# > 0)); do
@ -24,8 +24,8 @@ while (($# > 0)); do
$(basename "$0") [--docker-only|--hybrid] [项目名...]
说明:
--hybrid 宿主机构建 amd64容器构建 arm64默认
--docker-only 容器内同时构建 amd64 + arm64绕过宿主机依赖
--docker-only 双容器模式:容器内分别构建 amd64 + arm64默认
--hybrid 宿主机构建 amd64容器构建 arm64
EOF
exit 0
;;

@ -0,0 +1,8 @@
##step 1已完成
根据目标系统环境webkit2gtk 4.0 / javascriptcoregtk 4.0
- 将现有的call-client从tauri v2切换为使用tauri v1
- 将现有的broadcast-client从tauri v2切换为使用tauri v1
##step 2已完成
确认broadcast-client和calll-client都切换到tauri v1后修改scripts下的打包脚本继续沿用现在的双容器打包模式
Loading…
Cancel
Save