Compare commits
70 Commits
build
...
541e4e96cf
| Author | SHA1 | Date | |
|---|---|---|---|
| 541e4e96cf | |||
| 401eb3ab39 | |||
| d3eba96a29 | |||
| 81c02db00d | |||
|
|
9cb2ccbb4e | ||
| 8a10b8fe0f | |||
| 8fce8bdca4 | |||
| 1ab934cee9 | |||
| 6659ac6fad | |||
| 3a5ab1c614 | |||
| 1e07e1f833 | |||
| e1e91a3683 | |||
| c30d95a3e0 | |||
| 97f6fa843c | |||
| f43fc47539 | |||
|
|
c330de52fa | ||
| 67d35626cb | |||
| cc4c2189dc | |||
| d16905c0a3 | |||
| 4e611db349 | |||
| 7e9fc0ac3f | |||
| ff072d1a93 | |||
| a9c81c878e | |||
| 3660d13d7d | |||
| 281f53c049 | |||
| 71ca541f78 | |||
| 91f4f4afac | |||
| fc5639d7bd | |||
|
|
6668c11846 | ||
| 17f3351cea | |||
| dd3dd4ddb2 | |||
| 60d1494d45 | |||
| 1ef5350b3f | |||
| 3521e5787b | |||
| 8d9bcdad7e | |||
| ac086db1ed | |||
| 6dff0181d2 | |||
| ad24d3a140 | |||
| 4b0f39d747 | |||
| 096cc1da94 | |||
| 2d3200ad97 | |||
| 4e82e2f6f7 | |||
| 339ed53c2e | |||
| fc7c162e2f | |||
|
|
24f1549730 | ||
| 5584a46ca2 | |||
| 4471441d6f | |||
| 991a89147e | |||
| a08c0d8448 | |||
| 59db8dd177 | |||
| 29693f1baf | |||
| 5d6f157ae1 | |||
| afda3d5301 | |||
| 5d4ba757aa | |||
| d12d58b15a | |||
| 627c3dc71f | |||
| 26c7a3241c | |||
| 46c5e3dd1a | |||
|
|
92a6c6bfdb | ||
| 031aa49f9f | |||
| 1d7aee4cea | |||
| dec3ef5ef4 | |||
| d42f913250 | |||
| bae4e663fb | |||
| a17e060d16 | |||
| 71946965eb | |||
| d4cd22d234 | |||
| 05f2f7d46d | |||
| 9deb2744a9 | |||
| 6fac7c42d6 |
128
.github/workflows/build-release.yml
vendored
128
.github/workflows/build-release.yml
vendored
@@ -28,6 +28,7 @@ env:
|
||||
jobs:
|
||||
# 准备构建配置
|
||||
prepare:
|
||||
permissions: {}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -50,24 +51,27 @@ jobs:
|
||||
MATRIX='{"include":[]}'
|
||||
|
||||
if [[ "$PLATFORMS" == *"windows"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"windows-latest","os":"windows","arch":"amd64","output_name":"voidraft-windows-amd64.exe","id":"windows"}]')
|
||||
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"windows-latest","os":"windows","arch":"amd64","platform_dir":"windows","id":"windows"}]')
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORMS" == *"linux"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"ubuntu-22.04","os":"linux","arch":"amd64","output_name":"voidraft-linux-amd64","id":"linux"}]')
|
||||
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"ubuntu-22.04","os":"linux","arch":"amd64","platform_dir":"linux","id":"linux"}]')
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORMS" == *"macos-intel"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"macos-latest","os":"darwin","arch":"amd64","output_name":"voidraft-darwin-amd64","id":"macos-intel"}]')
|
||||
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"macos-latest","os":"darwin","arch":"amd64","platform_dir":"darwin","id":"macos-intel"}]')
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORMS" == *"macos-arm"* ]]; then
|
||||
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"macos-latest","os":"darwin","arch":"arm64","output_name":"voidraft-darwin-arm64","id":"macos-arm"}]')
|
||||
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"macos-latest","os":"darwin","arch":"arm64","platform_dir":"darwin","id":"macos-arm"}]')
|
||||
fi
|
||||
|
||||
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
|
||||
# 使用 -c 确保输出紧凑的单行 JSON,符合 GitHub Actions 输出格式
|
||||
echo "matrix=$(echo "$MATRIX" | jq -c .)" >> $GITHUB_OUTPUT
|
||||
|
||||
# 输出美化的 JSON 用于日志查看
|
||||
echo "生成的矩阵:"
|
||||
echo $MATRIX | jq .
|
||||
echo "$MATRIX" | jq .
|
||||
|
||||
- name: 检查是否创建 Release
|
||||
id: check-release
|
||||
@@ -81,6 +85,8 @@ jobs:
|
||||
fi
|
||||
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: prepare
|
||||
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
|
||||
strategy:
|
||||
@@ -113,17 +119,41 @@ jobs:
|
||||
if: matrix.os == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
# Wails 3 + 项目特定依赖
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.0-dev \
|
||||
pkg-config
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
libx11-dev \
|
||||
rpm \
|
||||
fuse \
|
||||
file
|
||||
|
||||
# Windows 平台依赖(GitHub Actions 的 Windows runner 已包含 MinGW)
|
||||
# 依赖说明:
|
||||
# - build-essential: C/C++ 编译工具链
|
||||
# - pkg-config: 包配置工具
|
||||
# - libgtk-3-dev: GTK3 GUI 框架
|
||||
# - libwebkit2gtk-4.1-dev: WebKit2GTK 4.1 (Wails 3 要求)
|
||||
# - libayatana-appindicator3-dev: 系统托盘支持
|
||||
# - librsvg2-dev: SVG 图标支持
|
||||
# - patchelf: 修改 ELF 二进制文件
|
||||
# - libx11-dev: X11 库 (热键服务依赖)
|
||||
# - rpm: RPM 打包工具
|
||||
# - fuse: AppImage 运行依赖
|
||||
# - file: 文件类型检测工具
|
||||
|
||||
# Windows 平台依赖
|
||||
- name: 设置 Windows 构建环境
|
||||
if: matrix.os == 'windows'
|
||||
run: |
|
||||
echo "Windows runner 已包含构建工具"
|
||||
# 安装 NSIS (用于创建安装程序)
|
||||
choco install nsis -y
|
||||
# 将 NSIS 添加到 PATH
|
||||
echo "C:\Program Files (x86)\NSIS" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
# macOS 平台依赖
|
||||
@@ -147,52 +177,64 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
|
||||
# 构建 Wails 应用
|
||||
- name: 构建 Wails 应用
|
||||
run: |
|
||||
wails3 build -platform ${{ matrix.os }}/${{ matrix.arch }}
|
||||
# 使用 Wails Task 构建和打包应用
|
||||
- name: 构建和打包 Wails 应用
|
||||
run: wails3 task ${{ matrix.id }}:package PRODUCTION=true ARCH=${{ matrix.arch }}
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
APP_NAME: voidraft
|
||||
BIN_DIR: bin
|
||||
ROOT_DIR: .
|
||||
shell: bash
|
||||
|
||||
# 查找构建产物
|
||||
- name: 查找构建产物
|
||||
id: find_binary
|
||||
shell: bash
|
||||
# 整理构建产物
|
||||
- name: 整理构建产物
|
||||
id: organize_artifacts
|
||||
run: |
|
||||
echo "=== 构建产物列表 ==="
|
||||
ls -lhR bin/ || echo "bin 目录不存在"
|
||||
|
||||
# 创建输出目录
|
||||
mkdir -p artifacts
|
||||
|
||||
# 根据平台复制产物
|
||||
if [ "${{ matrix.os }}" = "windows" ]; then
|
||||
BINARY=$(find build/bin -name "*.exe" -type f | head -n 1)
|
||||
elif [ "${{ matrix.os }}" = "darwin" ]; then
|
||||
# macOS 可能生成 .app 包或二进制文件
|
||||
BINARY=$(find build/bin -type f \( -name "*.app" -o ! -name ".*" \) | head -n 1)
|
||||
else
|
||||
BINARY=$(find build/bin -type f ! -name ".*" | head -n 1)
|
||||
fi
|
||||
echo "binary_path=$BINARY" >> $GITHUB_OUTPUT
|
||||
echo "找到的二进制文件: $BINARY"
|
||||
echo "Windows 平台:查找 NSIS 安装程序"
|
||||
find . -name "*.exe" -type f
|
||||
cp bin/*.exe artifacts/ 2>/dev/null || echo "未找到 .exe 文件"
|
||||
|
||||
# 重命名构建产物
|
||||
- name: 重命名构建产物
|
||||
shell: bash
|
||||
run: |
|
||||
BINARY_PATH="${{ steps.find_binary.outputs.binary_path }}"
|
||||
if [ -n "$BINARY_PATH" ]; then
|
||||
# 对于 macOS .app 包,打包为 zip
|
||||
if [[ "$BINARY_PATH" == *.app ]]; then
|
||||
cd "$(dirname "$BINARY_PATH")"
|
||||
zip -r "${{ matrix.output_name }}.zip" "$(basename "$BINARY_PATH")"
|
||||
echo "ARTIFACT_PATH=$(pwd)/${{ matrix.output_name }}.zip" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.os }}" = "linux" ]; then
|
||||
echo "Linux 平台:查找 AppImage, deb, rpm, archlinux 包"
|
||||
find bin -type f
|
||||
cp bin/*.AppImage artifacts/ 2>/dev/null || echo "未找到 AppImage"
|
||||
cp bin/*.deb artifacts/ 2>/dev/null || echo "未找到 deb"
|
||||
cp bin/*.rpm artifacts/ 2>/dev/null || echo "未找到 rpm"
|
||||
cp bin/*.pkg.tar.zst artifacts/ 2>/dev/null || echo "未找到 archlinux 包"
|
||||
|
||||
elif [ "${{ matrix.os }}" = "darwin" ]; then
|
||||
echo "macOS 平台:查找 .app bundle"
|
||||
find bin -name "*.app" -type d
|
||||
# macOS: .app bundle,打包成 zip
|
||||
if [ -d "bin/voidraft.app" ]; then
|
||||
cd bin
|
||||
zip -r ../artifacts/voidraft-darwin-${{ matrix.arch }}.app.zip voidraft.app
|
||||
cd ..
|
||||
else
|
||||
cp "$BINARY_PATH" "${{ matrix.output_name }}"
|
||||
echo "ARTIFACT_PATH=$(pwd)/${{ matrix.output_name }}" >> $GITHUB_ENV
|
||||
echo "未找到 .app bundle"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== 最终产物 ==="
|
||||
ls -lh artifacts/ || echo "artifacts 目录为空"
|
||||
shell: bash
|
||||
|
||||
# 上传构建产物到 Artifacts
|
||||
- name: 上传构建产物
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.output_name }}
|
||||
path: ${{ env.ARTIFACT_PATH }}
|
||||
if-no-files-found: error
|
||||
name: voidraft-${{ matrix.id }}
|
||||
path: artifacts/*
|
||||
if-no-files-found: warn
|
||||
|
||||
# 创建 GitHub Release 并上传所有构建产物
|
||||
release:
|
||||
|
||||
109
.github/workflows/codeql.yml
vendored
Normal file
109
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '29 8 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: c-cpp
|
||||
build-mode: none
|
||||
- language: go
|
||||
build-mode: autobuild
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
- language: python
|
||||
build-mode: none
|
||||
- language: rust
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- name: Run manual build steps
|
||||
if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -159,5 +159,8 @@ Welcome to Fork, Star, and contribute code.
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/yourusername/voidraft)
|
||||
[](https://github.com/yourusername/voidraft)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Flandaiqing%2Fvoidraft?ref=badge_shield)
|
||||
|
||||
*Made with ❤️ by landaiqing*
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Flandaiqing%2Fvoidraft?ref=badge_large)
|
||||
12
Taskfile.yml
12
Taskfile.yml
@@ -12,25 +12,13 @@ vars:
|
||||
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||
|
||||
tasks:
|
||||
version:
|
||||
summary: Generate version information
|
||||
cmds:
|
||||
- '{{if eq OS "windows"}}cmd /c ".\scripts\version.bat"{{else}}bash ./scripts/version.sh{{end}}'
|
||||
sources:
|
||||
- scripts/version.bat
|
||||
- scripts/version.sh
|
||||
generates:
|
||||
- version.txt
|
||||
|
||||
build:
|
||||
summary: Builds the application
|
||||
deps: [version]
|
||||
cmds:
|
||||
- task: "{{OS}}:build"
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application
|
||||
deps: [version]
|
||||
cmds:
|
||||
- task: "{{OS}}:package"
|
||||
|
||||
|
||||
293
build/COMMANDS.md
Normal file
293
build/COMMANDS.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Wails 3 命令参考表
|
||||
|
||||
本文档列出了 Voidraft 项目中使用的所有 Wails 3 命令和参数。
|
||||
|
||||
## 📋 命令总览
|
||||
|
||||
| 类别 | 命令 | 用途 |
|
||||
|------|------|------|
|
||||
| 生成工具 | `wails3 generate` | 生成项目所需的各种资源文件 |
|
||||
| 打包工具 | `wails3 tool package` | 使用 nfpm 打包应用程序 |
|
||||
| 任务执行 | `wails3 task` | 执行 Taskfile.yml 中定义的任务 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 详细命令参数
|
||||
|
||||
### 1. `wails3 generate bindings`
|
||||
**生成 TypeScript 绑定文件**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-f` | 构建标志 | `''` (空字符串) |
|
||||
| `-clean` | 清理旧绑定 | `true` |
|
||||
| `-ts` | 生成 TypeScript | 无需值 |
|
||||
|
||||
**使用位置:** `build/Taskfile.yml:53`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 generate bindings -f '' -clean=true -ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `wails3 generate icons`
|
||||
**从单个图片生成多平台图标**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-input` | 输入图片路径 | `appicon.png` |
|
||||
| `-macfilename` | macOS 图标输出路径 | `darwin/icons.icns` |
|
||||
| `-windowsfilename` | Windows 图标输出路径 | `windows/icons.ico` |
|
||||
|
||||
**使用位置:** `build/Taskfile.yml:64`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 generate icons \
|
||||
-input appicon.png \
|
||||
-macfilename darwin/icons.icns \
|
||||
-windowsfilename windows/icons.ico
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `wails3 generate syso`
|
||||
**生成 Windows .syso 资源文件**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-arch` | 目标架构 | `amd64` / `arm64` |
|
||||
| `-icon` | 图标文件 | `windows/icon.ico` |
|
||||
| `-manifest` | 清单文件 | `windows/wails.exe.manifest` |
|
||||
| `-info` | 应用信息 JSON | `windows/info.json` |
|
||||
| `-out` | 输出文件路径 | `../wails_windows_amd64.syso` |
|
||||
|
||||
**使用位置:** `build/windows/Taskfile.yml:42`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 generate syso \
|
||||
-arch amd64 \
|
||||
-icon windows/icon.ico \
|
||||
-manifest windows/wails.exe.manifest \
|
||||
-info windows/info.json \
|
||||
-out ../wails_windows_amd64.syso
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `wails3 generate webview2bootstrapper`
|
||||
**生成 Windows WebView2 引导程序**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-dir` | 输出目录 | `build/windows/nsis` |
|
||||
|
||||
**使用位置:** `build/windows/Taskfile.yml:55`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 generate webview2bootstrapper -dir "build/windows/nsis"
|
||||
```
|
||||
|
||||
**说明:** 下载 Microsoft Edge WebView2 运行时安装程序,用于 NSIS 打包。
|
||||
|
||||
---
|
||||
|
||||
### 5. `wails3 generate .desktop`
|
||||
**生成 Linux .desktop 桌面文件**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-name` | 应用名称 | `voidraft` |
|
||||
| `-exec` | 可执行文件名 | `voidraft` |
|
||||
| `-icon` | 图标名称 | `appicon` |
|
||||
| `-outputfile` | 输出文件路径 | `build/linux/voidraft.desktop` |
|
||||
| `-categories` | 应用分类 | `Development;` |
|
||||
|
||||
**使用位置:** `build/linux/Taskfile.yml:107`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 generate .desktop \
|
||||
-name "voidraft" \
|
||||
-exec "voidraft" \
|
||||
-icon "appicon" \
|
||||
-outputfile build/linux/voidraft.desktop \
|
||||
-categories "Development;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `wails3 generate appimage`
|
||||
**生成 Linux AppImage 包**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-binary` | 二进制文件名 | `voidraft` |
|
||||
| `-icon` | 图标文件路径 | `../../appicon.png` |
|
||||
| `-desktopfile` | .desktop 文件路径 | `../voidraft.desktop` |
|
||||
| `-outputdir` | 输出目录 | `../../../bin` |
|
||||
| `-builddir` | 构建临时目录 | `build/linux/appimage/build` |
|
||||
|
||||
**使用位置:** `build/linux/Taskfile.yml:49`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 generate appimage \
|
||||
-binary voidraft \
|
||||
-icon ../../appicon.png \
|
||||
-desktopfile ../voidraft.desktop \
|
||||
-outputdir ../../../bin \
|
||||
-builddir build/linux/appimage/build
|
||||
```
|
||||
|
||||
**说明:** 自动下载 linuxdeploy 工具并创建 AppImage。
|
||||
|
||||
---
|
||||
|
||||
### 7. `wails3 tool package` (deb)
|
||||
**创建 Debian/Ubuntu .deb 包**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-name` | 包名称 | `voidraft` |
|
||||
| `-format` | 包格式 | `deb` |
|
||||
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
|
||||
| `-out` | 输出目录 | `./bin` |
|
||||
|
||||
**使用位置:** `build/linux/Taskfile.yml:90`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 tool package \
|
||||
-name voidraft \
|
||||
-format deb \
|
||||
-config ./build/linux/nfpm/nfpm.yaml \
|
||||
-out ./bin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. `wails3 tool package` (rpm)
|
||||
**创建 RedHat/CentOS/Fedora .rpm 包**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-name` | 包名称 | `voidraft` |
|
||||
| `-format` | 包格式 | `rpm` |
|
||||
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
|
||||
| `-out` | 输出目录 | `./bin` |
|
||||
|
||||
**使用位置:** `build/linux/Taskfile.yml:95`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 tool package \
|
||||
-name voidraft \
|
||||
-format rpm \
|
||||
-config ./build/linux/nfpm/nfpm.yaml \
|
||||
-out ./bin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. `wails3 tool package` (archlinux)
|
||||
**创建 Arch Linux .pkg.tar.zst 包**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `-name` | 包名称 | `voidraft` |
|
||||
| `-format` | 包格式 | `archlinux` |
|
||||
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
|
||||
| `-out` | 输出目录 | `./bin` |
|
||||
|
||||
**使用位置:** `build/linux/Taskfile.yml:100`
|
||||
|
||||
**完整命令:**
|
||||
```bash
|
||||
wails3 tool package \
|
||||
-name voidraft \
|
||||
-format archlinux \
|
||||
-config ./build/linux/nfpm/nfpm.yaml \
|
||||
-out ./bin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. `wails3 task`
|
||||
**执行 Taskfile.yml 中定义的任务**
|
||||
|
||||
| 参数 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| `[taskname]` | 任务名称 | `build`, `package`, `run` |
|
||||
| `[VAR=value]` | 变量赋值 | `PRODUCTION=true`, `ARCH=amd64` |
|
||||
|
||||
**常用任务:**
|
||||
|
||||
| 任务 | 说明 | 命令 |
|
||||
|------|------|------|
|
||||
| `build` | 构建应用 | `wails3 task build PRODUCTION=true` |
|
||||
| `package` | 打包应用 | `wails3 task package` |
|
||||
| `run` | 运行应用 | `wails3 task run` |
|
||||
|
||||
**使用位置:** `build/config.yml`
|
||||
|
||||
---
|
||||
|
||||
## 📊 平台对应命令表
|
||||
|
||||
| 平台 | 主要命令 | 产物 |
|
||||
|------|----------|------|
|
||||
| **Windows** | `generate syso`<br>`generate webview2bootstrapper` | `.syso` 资源文件<br>NSIS 安装程序 |
|
||||
| **Linux** | `generate .desktop`<br>`generate appimage`<br>`tool package -format deb/rpm/archlinux` | `.desktop` 文件<br>`.AppImage`<br>`.deb` / `.rpm` / `.pkg.tar.zst` |
|
||||
| **macOS** | `generate icons` | `.icns` 图标<br>`.app` 应用包 |
|
||||
| **通用** | `generate bindings`<br>`generate icons` | TypeScript 绑定<br>多平台图标 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速参考
|
||||
|
||||
### 完整构建流程
|
||||
|
||||
```bash
|
||||
# 1. 生成绑定和图标
|
||||
wails3 task common:generate:bindings
|
||||
wails3 task common:generate:icons
|
||||
|
||||
# 2. 构建前端
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# 3. 构建应用(各平台)
|
||||
cd build/windows && wails3 task build PRODUCTION=true # Windows
|
||||
cd build/linux && wails3 task build PRODUCTION=true # Linux
|
||||
cd build/darwin && wails3 task build PRODUCTION=true # macOS
|
||||
|
||||
# 4. 打包应用(各平台)
|
||||
cd build/windows && wails3 task package # NSIS 安装程序
|
||||
cd build/linux && wails3 task package # AppImage + deb + rpm + archlinux
|
||||
cd build/darwin && wails3 task package # .app bundle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **变量传递:** Task 命令支持通过 `VAR=value` 格式传递变量
|
||||
2. **路径问题:** 相对路径基于 Taskfile.yml 所在目录
|
||||
3. **依赖顺序:** 某些任务有依赖关系(通过 `deps:` 定义)
|
||||
4. **环境变量:** 使用 `env:` 定义的环境变量会自动设置
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [Wails 3 官方文档](https://v3alpha.wails.io/)
|
||||
- [Taskfile 语法](https://taskfile.dev/)
|
||||
- [nfpm 打包工具](https://nfpm.goreleaser.com/)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 6.9 KiB |
@@ -61,5 +61,3 @@ export class ServiceOptions {
|
||||
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
export type Window = any;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
17
frontend/bindings/voidraft/internal/common/helper/models.ts
Normal file
17
frontend/bindings/voidraft/internal/common/helper/models.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
4
frontend/bindings/voidraft/internal/models/ent/index.ts
Normal file
4
frontend/bindings/voidraft/internal/models/ent/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
334
frontend/bindings/voidraft/internal/models/ent/models.ts
Normal file
334
frontend/bindings/voidraft/internal/models/ent/models.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as theme$0 from "./theme/models.js";
|
||||
|
||||
/**
|
||||
* Document is the model entity for the Document schema.
|
||||
*/
|
||||
export class Document {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* document title
|
||||
*/
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* document content
|
||||
*/
|
||||
"content": string;
|
||||
|
||||
/**
|
||||
* document locked status
|
||||
*/
|
||||
"locked": boolean;
|
||||
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
if (!("content" in $$source)) {
|
||||
this["content"] = "";
|
||||
}
|
||||
if (!("locked" in $$source)) {
|
||||
this["locked"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Document instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Document {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Document($$parsedSource as Partial<Document>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension is the model entity for the Extension schema.
|
||||
*/
|
||||
export class Extension {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* extension key
|
||||
*/
|
||||
"key": string;
|
||||
|
||||
/**
|
||||
* extension enabled or not
|
||||
*/
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* extension config
|
||||
*/
|
||||
"config": { [_: string]: any };
|
||||
|
||||
/** Creates a new Extension instance. */
|
||||
constructor($$source: Partial<Extension> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("config" in $$source)) {
|
||||
this["config"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Extension instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Extension {
|
||||
const $$createField7_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("config" in $$parsedSource) {
|
||||
$$parsedSource["config"] = $$createField7_0($$parsedSource["config"]);
|
||||
}
|
||||
return new Extension($$parsedSource as Partial<Extension>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyBinding is the model entity for the KeyBinding schema.
|
||||
*/
|
||||
export class KeyBinding {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* key binding key
|
||||
*/
|
||||
"key": string;
|
||||
|
||||
/**
|
||||
* key binding command
|
||||
*/
|
||||
"command": string;
|
||||
|
||||
/**
|
||||
* key binding extension
|
||||
*/
|
||||
"extension"?: string;
|
||||
|
||||
/**
|
||||
* key binding enabled
|
||||
*/
|
||||
"enabled": boolean;
|
||||
|
||||
/** Creates a new KeyBinding instance. */
|
||||
constructor($$source: Partial<KeyBinding> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
}
|
||||
if (!("command" in $$source)) {
|
||||
this["command"] = "";
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new KeyBinding instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): KeyBinding {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new KeyBinding($$parsedSource as Partial<KeyBinding>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme is the model entity for the Theme schema.
|
||||
*/
|
||||
export class Theme {
|
||||
/**
|
||||
* ID of the ent.
|
||||
*/
|
||||
"id"?: number;
|
||||
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
*/
|
||||
"created_at": string;
|
||||
|
||||
/**
|
||||
* update time
|
||||
*/
|
||||
"updated_at": string;
|
||||
|
||||
/**
|
||||
* deleted at
|
||||
*/
|
||||
"deleted_at"?: string | null;
|
||||
|
||||
/**
|
||||
* theme key
|
||||
*/
|
||||
"key": string;
|
||||
|
||||
/**
|
||||
* theme type
|
||||
*/
|
||||
"type": theme$0.Type;
|
||||
|
||||
/**
|
||||
* theme colors
|
||||
*/
|
||||
"colors": { [_: string]: any };
|
||||
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
}
|
||||
if (!("updated_at" in $$source)) {
|
||||
this["updated_at"] = "";
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = ("" as theme$0.Type);
|
||||
}
|
||||
if (!("colors" in $$source)) {
|
||||
this["colors"] = {};
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Theme instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Theme {
|
||||
const $$createField7_0 = $$createType0;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("colors" in $$parsedSource) {
|
||||
$$parsedSource["colors"] = $$createField7_0($$parsedSource["colors"]);
|
||||
}
|
||||
return new Theme($$parsedSource as Partial<Theme>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
@@ -0,0 +1,22 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* Type defines the type for the "type" enum field.
|
||||
*/
|
||||
export enum Type {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Type values.
|
||||
*/
|
||||
TypeDark = "dark",
|
||||
TypeLight = "light",
|
||||
};
|
||||
@@ -193,58 +193,6 @@ export class ConfigMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Document represents a document in the system
|
||||
*/
|
||||
export class Document {
|
||||
"id": number;
|
||||
"title": string;
|
||||
"content": string;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
"is_deleted": boolean;
|
||||
|
||||
/**
|
||||
* 锁定标志,锁定的文档无法被删除
|
||||
*/
|
||||
"is_locked": boolean;
|
||||
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = 0;
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
if (!("content" in $$source)) {
|
||||
this["content"] = "";
|
||||
}
|
||||
if (!("createdAt" in $$source)) {
|
||||
this["createdAt"] = "";
|
||||
}
|
||||
if (!("updatedAt" in $$source)) {
|
||||
this["updatedAt"] = "";
|
||||
}
|
||||
if (!("is_deleted" in $$source)) {
|
||||
this["is_deleted"] = false;
|
||||
}
|
||||
if (!("is_locked" in $$source)) {
|
||||
this["is_locked"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Document instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Document {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Document($$parsedSource as Partial<Document>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EditingConfig 编辑设置配置
|
||||
*/
|
||||
@@ -332,40 +280,21 @@ export class EditingConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension 单个扩展配置
|
||||
* Extension 扩展配置
|
||||
*/
|
||||
export class Extension {
|
||||
/**
|
||||
* 扩展唯一标识
|
||||
*/
|
||||
"id": ExtensionID;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
"key": ExtensionKey;
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* 是否为默认扩展
|
||||
*/
|
||||
"isDefault": boolean;
|
||||
|
||||
/**
|
||||
* 扩展配置项
|
||||
*/
|
||||
"config": ExtensionConfig;
|
||||
|
||||
/** Creates a new Extension instance. */
|
||||
constructor($$source: Partial<Extension> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = ("" as ExtensionID);
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = ("" as ExtensionKey);
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("isDefault" in $$source)) {
|
||||
this["isDefault"] = false;
|
||||
}
|
||||
if (!("config" in $$source)) {
|
||||
this["config"] = ({} as ExtensionConfig);
|
||||
}
|
||||
@@ -377,10 +306,10 @@ export class Extension {
|
||||
* Creates a new Extension instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Extension {
|
||||
const $$createField3_0 = $$createType6;
|
||||
const $$createField2_0 = $$createType6;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("config" in $$parsedSource) {
|
||||
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
||||
$$parsedSource["config"] = $$createField2_0($$parsedSource["config"]);
|
||||
}
|
||||
return new Extension($$parsedSource as Partial<Extension>);
|
||||
}
|
||||
@@ -392,9 +321,9 @@ export class Extension {
|
||||
export type ExtensionConfig = { [_: string]: any };
|
||||
|
||||
/**
|
||||
* ExtensionID 扩展标识符
|
||||
* ExtensionKey 扩展标识符
|
||||
*/
|
||||
export enum ExtensionID {
|
||||
export enum ExtensionKey {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
@@ -415,13 +344,11 @@ export enum ExtensionID {
|
||||
* 颜色选择器
|
||||
*/
|
||||
ExtensionColorSelector = "colorSelector",
|
||||
ExtensionFold = "fold",
|
||||
ExtensionTextHighlight = "textHighlight",
|
||||
|
||||
/**
|
||||
* 选择框
|
||||
* 代码折叠
|
||||
*/
|
||||
ExtensionCheckbox = "checkbox",
|
||||
ExtensionFold = "fold",
|
||||
|
||||
/**
|
||||
* 划词翻译
|
||||
@@ -429,22 +356,44 @@ export enum ExtensionID {
|
||||
ExtensionTranslator = "translator",
|
||||
|
||||
/**
|
||||
* UI增强扩展
|
||||
* Markdown渲染
|
||||
*/
|
||||
ExtensionMarkdown = "markdown",
|
||||
|
||||
/**
|
||||
* 显示空白字符
|
||||
*/
|
||||
ExtensionHighlightWhitespace = "highlightWhitespace",
|
||||
|
||||
/**
|
||||
* 高亮行尾空白
|
||||
*/
|
||||
ExtensionHighlightTrailingWhitespace = "highlightTrailingWhitespace",
|
||||
|
||||
/**
|
||||
* 小地图
|
||||
*/
|
||||
ExtensionMinimap = "minimap",
|
||||
|
||||
/**
|
||||
* 工具扩展
|
||||
* 行号显示
|
||||
*/
|
||||
ExtensionLineNumbers = "lineNumbers",
|
||||
|
||||
/**
|
||||
* 上下文菜单
|
||||
*/
|
||||
ExtensionContextMenu = "contextMenu",
|
||||
|
||||
/**
|
||||
* 搜索功能
|
||||
*/
|
||||
ExtensionSearch = "search",
|
||||
|
||||
/**
|
||||
* 核心扩展
|
||||
* 编辑器核心功能
|
||||
* HTTP 客户端
|
||||
*/
|
||||
ExtensionEditor = "editor",
|
||||
ExtensionHttpClient = "httpClient",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -735,48 +684,25 @@ export class HotkeyCombo {
|
||||
* KeyBinding 单个快捷键绑定
|
||||
*/
|
||||
export class KeyBinding {
|
||||
/**
|
||||
* 快捷键动作
|
||||
*/
|
||||
"command": KeyBindingCommand;
|
||||
|
||||
/**
|
||||
* 所属扩展
|
||||
*/
|
||||
"extension": ExtensionID;
|
||||
|
||||
/**
|
||||
* 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
||||
*/
|
||||
"key": string;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
"key": KeyBindingKey;
|
||||
"command": string;
|
||||
"extension": ExtensionKey;
|
||||
"enabled": boolean;
|
||||
|
||||
/**
|
||||
* 是否为默认快捷键
|
||||
*/
|
||||
"isDefault": boolean;
|
||||
|
||||
/** Creates a new KeyBinding instance. */
|
||||
constructor($$source: Partial<KeyBinding> = {}) {
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = ("" as KeyBindingKey);
|
||||
}
|
||||
if (!("command" in $$source)) {
|
||||
this["command"] = ("" as KeyBindingCommand);
|
||||
this["command"] = "";
|
||||
}
|
||||
if (!("extension" in $$source)) {
|
||||
this["extension"] = ("" as ExtensionID);
|
||||
}
|
||||
if (!("key" in $$source)) {
|
||||
this["key"] = "";
|
||||
this["extension"] = ("" as ExtensionKey);
|
||||
}
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("isDefault" in $$source)) {
|
||||
this["isDefault"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -791,294 +717,258 @@ export class KeyBinding {
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyBindingCommand 快捷键命令
|
||||
* KeyBindingKey 快捷键命令
|
||||
*/
|
||||
export enum KeyBindingCommand {
|
||||
export enum KeyBindingKey {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* 搜索扩展相关
|
||||
* 显示搜索
|
||||
*/
|
||||
ShowSearchCommand = "showSearch",
|
||||
ShowSearchKeyBindingKey = "showSearch",
|
||||
|
||||
/**
|
||||
* 隐藏搜索
|
||||
*/
|
||||
HideSearchCommand = "hideSearch",
|
||||
HideSearchKeyBindingKey = "hideSearch",
|
||||
|
||||
/**
|
||||
* 搜索切换大小写
|
||||
*/
|
||||
SearchToggleCaseCommand = "searchToggleCase",
|
||||
|
||||
/**
|
||||
* 搜索切换整词
|
||||
*/
|
||||
SearchToggleWordCommand = "searchToggleWord",
|
||||
|
||||
/**
|
||||
* 搜索切换正则
|
||||
*/
|
||||
SearchToggleRegexCommand = "searchToggleRegex",
|
||||
|
||||
/**
|
||||
* 显示替换
|
||||
*/
|
||||
SearchShowReplaceCommand = "searchShowReplace",
|
||||
|
||||
/**
|
||||
* 替换全部
|
||||
*/
|
||||
SearchReplaceAllCommand = "searchReplaceAll",
|
||||
|
||||
/**
|
||||
* 代码块扩展相关
|
||||
* 块内选择全部
|
||||
*/
|
||||
BlockSelectAllCommand = "blockSelectAll",
|
||||
BlockSelectAllKeyBindingKey = "blockSelectAll",
|
||||
|
||||
/**
|
||||
* 在当前块后添加新块
|
||||
*/
|
||||
BlockAddAfterCurrentCommand = "blockAddAfterCurrent",
|
||||
BlockAddAfterCurrentKeyBindingKey = "blockAddAfterCurrent",
|
||||
|
||||
/**
|
||||
* 在最后添加新块
|
||||
*/
|
||||
BlockAddAfterLastCommand = "blockAddAfterLast",
|
||||
BlockAddAfterLastKeyBindingKey = "blockAddAfterLast",
|
||||
|
||||
/**
|
||||
* 在当前块前添加新块
|
||||
*/
|
||||
BlockAddBeforeCurrentCommand = "blockAddBeforeCurrent",
|
||||
BlockAddBeforeCurrentKeyBindingKey = "blockAddBeforeCurrent",
|
||||
|
||||
/**
|
||||
* 跳转到上一个块
|
||||
*/
|
||||
BlockGotoPreviousCommand = "blockGotoPrevious",
|
||||
BlockGotoPreviousKeyBindingKey = "blockGotoPrevious",
|
||||
|
||||
/**
|
||||
* 跳转到下一个块
|
||||
*/
|
||||
BlockGotoNextCommand = "blockGotoNext",
|
||||
BlockGotoNextKeyBindingKey = "blockGotoNext",
|
||||
|
||||
/**
|
||||
* 选择上一个块
|
||||
*/
|
||||
BlockSelectPreviousCommand = "blockSelectPrevious",
|
||||
BlockSelectPreviousKeyBindingKey = "blockSelectPrevious",
|
||||
|
||||
/**
|
||||
* 选择下一个块
|
||||
*/
|
||||
BlockSelectNextCommand = "blockSelectNext",
|
||||
BlockSelectNextKeyBindingKey = "blockSelectNext",
|
||||
|
||||
/**
|
||||
* 删除当前块
|
||||
*/
|
||||
BlockDeleteCommand = "blockDelete",
|
||||
BlockDeleteKeyBindingKey = "blockDelete",
|
||||
|
||||
/**
|
||||
* 向上移动当前块
|
||||
*/
|
||||
BlockMoveUpCommand = "blockMoveUp",
|
||||
BlockMoveUpKeyBindingKey = "blockMoveUp",
|
||||
|
||||
/**
|
||||
* 向下移动当前块
|
||||
*/
|
||||
BlockMoveDownCommand = "blockMoveDown",
|
||||
BlockMoveDownKeyBindingKey = "blockMoveDown",
|
||||
|
||||
/**
|
||||
* 删除行
|
||||
*/
|
||||
BlockDeleteLineCommand = "blockDeleteLine",
|
||||
BlockDeleteLineKeyBindingKey = "blockDeleteLine",
|
||||
|
||||
/**
|
||||
* 向上移动行
|
||||
*/
|
||||
BlockMoveLineUpCommand = "blockMoveLineUp",
|
||||
BlockMoveLineUpKeyBindingKey = "blockMoveLineUp",
|
||||
|
||||
/**
|
||||
* 向下移动行
|
||||
*/
|
||||
BlockMoveLineDownCommand = "blockMoveLineDown",
|
||||
BlockMoveLineDownKeyBindingKey = "blockMoveLineDown",
|
||||
|
||||
/**
|
||||
* 字符转置
|
||||
*/
|
||||
BlockTransposeCharsCommand = "blockTransposeChars",
|
||||
BlockTransposeCharsKeyBindingKey = "blockTransposeChars",
|
||||
|
||||
/**
|
||||
* 格式化代码块
|
||||
*/
|
||||
BlockFormatCommand = "blockFormat",
|
||||
BlockFormatKeyBindingKey = "blockFormat",
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
BlockCopyCommand = "blockCopy",
|
||||
BlockCopyKeyBindingKey = "blockCopy",
|
||||
|
||||
/**
|
||||
* 剪切
|
||||
*/
|
||||
BlockCutCommand = "blockCut",
|
||||
BlockCutKeyBindingKey = "blockCut",
|
||||
|
||||
/**
|
||||
* 粘贴
|
||||
*/
|
||||
BlockPasteCommand = "blockPaste",
|
||||
BlockPasteKeyBindingKey = "blockPaste",
|
||||
|
||||
/**
|
||||
* 代码折叠扩展相关
|
||||
* 折叠代码
|
||||
*/
|
||||
FoldCodeCommand = "foldCode",
|
||||
FoldCodeKeyBindingKey = "foldCode",
|
||||
|
||||
/**
|
||||
* 展开代码
|
||||
*/
|
||||
UnfoldCodeCommand = "unfoldCode",
|
||||
UnfoldCodeKeyBindingKey = "unfoldCode",
|
||||
|
||||
/**
|
||||
* 折叠全部
|
||||
*/
|
||||
FoldAllCommand = "foldAll",
|
||||
FoldAllKeyBindingKey = "foldAll",
|
||||
|
||||
/**
|
||||
* 展开全部
|
||||
*/
|
||||
UnfoldAllCommand = "unfoldAll",
|
||||
UnfoldAllKeyBindingKey = "unfoldAll",
|
||||
|
||||
/**
|
||||
* 通用编辑扩展相关
|
||||
* 光标按语法左移
|
||||
*/
|
||||
CursorSyntaxLeftCommand = "cursorSyntaxLeft",
|
||||
CursorSyntaxLeftKeyBindingKey = "cursorSyntaxLeft",
|
||||
|
||||
/**
|
||||
* 光标按语法右移
|
||||
*/
|
||||
CursorSyntaxRightCommand = "cursorSyntaxRight",
|
||||
CursorSyntaxRightKeyBindingKey = "cursorSyntaxRight",
|
||||
|
||||
/**
|
||||
* 按语法选择左侧
|
||||
*/
|
||||
SelectSyntaxLeftCommand = "selectSyntaxLeft",
|
||||
SelectSyntaxLeftKeyBindingKey = "selectSyntaxLeft",
|
||||
|
||||
/**
|
||||
* 按语法选择右侧
|
||||
*/
|
||||
SelectSyntaxRightCommand = "selectSyntaxRight",
|
||||
SelectSyntaxRightKeyBindingKey = "selectSyntaxRight",
|
||||
|
||||
/**
|
||||
* 向上复制行
|
||||
*/
|
||||
CopyLineUpCommand = "copyLineUp",
|
||||
CopyLineUpKeyBindingKey = "copyLineUp",
|
||||
|
||||
/**
|
||||
* 向下复制行
|
||||
*/
|
||||
CopyLineDownCommand = "copyLineDown",
|
||||
CopyLineDownKeyBindingKey = "copyLineDown",
|
||||
|
||||
/**
|
||||
* 插入空行
|
||||
*/
|
||||
InsertBlankLineCommand = "insertBlankLine",
|
||||
InsertBlankLineKeyBindingKey = "insertBlankLine",
|
||||
|
||||
/**
|
||||
* 选择行
|
||||
*/
|
||||
SelectLineCommand = "selectLine",
|
||||
SelectLineKeyBindingKey = "selectLine",
|
||||
|
||||
/**
|
||||
* 选择父级语法
|
||||
*/
|
||||
SelectParentSyntaxCommand = "selectParentSyntax",
|
||||
SelectParentSyntaxKeyBindingKey = "selectParentSyntax",
|
||||
|
||||
/**
|
||||
* 减少缩进
|
||||
*/
|
||||
IndentLessCommand = "indentLess",
|
||||
IndentLessKeyBindingKey = "indentLess",
|
||||
|
||||
/**
|
||||
* 增加缩进
|
||||
*/
|
||||
IndentMoreCommand = "indentMore",
|
||||
IndentMoreKeyBindingKey = "indentMore",
|
||||
|
||||
/**
|
||||
* 缩进选择
|
||||
*/
|
||||
IndentSelectionCommand = "indentSelection",
|
||||
IndentSelectionKeyBindingKey = "indentSelection",
|
||||
|
||||
/**
|
||||
* 光标到匹配括号
|
||||
*/
|
||||
CursorMatchingBracketCommand = "cursorMatchingBracket",
|
||||
CursorMatchingBracketKeyBindingKey = "cursorMatchingBracket",
|
||||
|
||||
/**
|
||||
* 切换注释
|
||||
*/
|
||||
ToggleCommentCommand = "toggleComment",
|
||||
ToggleCommentKeyBindingKey = "toggleComment",
|
||||
|
||||
/**
|
||||
* 切换块注释
|
||||
*/
|
||||
ToggleBlockCommentCommand = "toggleBlockComment",
|
||||
ToggleBlockCommentKeyBindingKey = "toggleBlockComment",
|
||||
|
||||
/**
|
||||
* 插入新行并缩进
|
||||
*/
|
||||
InsertNewlineAndIndentCommand = "insertNewlineAndIndent",
|
||||
InsertNewlineAndIndentKeyBindingKey = "insertNewlineAndIndent",
|
||||
|
||||
/**
|
||||
* 向后删除字符
|
||||
*/
|
||||
DeleteCharBackwardCommand = "deleteCharBackward",
|
||||
DeleteCharBackwardKeyBindingKey = "deleteCharBackward",
|
||||
|
||||
/**
|
||||
* 向前删除字符
|
||||
*/
|
||||
DeleteCharForwardCommand = "deleteCharForward",
|
||||
DeleteCharForwardKeyBindingKey = "deleteCharForward",
|
||||
|
||||
/**
|
||||
* 向后删除组
|
||||
*/
|
||||
DeleteGroupBackwardCommand = "deleteGroupBackward",
|
||||
DeleteGroupBackwardKeyBindingKey = "deleteGroupBackward",
|
||||
|
||||
/**
|
||||
* 向前删除组
|
||||
*/
|
||||
DeleteGroupForwardCommand = "deleteGroupForward",
|
||||
DeleteGroupForwardKeyBindingKey = "deleteGroupForward",
|
||||
|
||||
/**
|
||||
* 历史记录扩展相关
|
||||
* 撤销
|
||||
*/
|
||||
HistoryUndoCommand = "historyUndo",
|
||||
HistoryUndoKeyBindingKey = "historyUndo",
|
||||
|
||||
/**
|
||||
* 重做
|
||||
*/
|
||||
HistoryRedoCommand = "historyRedo",
|
||||
HistoryRedoKeyBindingKey = "historyRedo",
|
||||
|
||||
/**
|
||||
* 撤销选择
|
||||
*/
|
||||
HistoryUndoSelectionCommand = "historyUndoSelection",
|
||||
HistoryUndoSelectionKeyBindingKey = "historyUndoSelection",
|
||||
|
||||
/**
|
||||
* 重做选择
|
||||
*/
|
||||
HistoryRedoSelectionCommand = "historyRedoSelection",
|
||||
|
||||
/**
|
||||
* 文本高亮扩展相关
|
||||
* 切换文本高亮
|
||||
*/
|
||||
TextHighlightToggleCommand = "textHighlightToggle",
|
||||
HistoryRedoSelectionKeyBindingKey = "historyRedoSelection",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1146,370 +1036,6 @@ export enum TabType {
|
||||
TabTypeTab = "tab",
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme 主题数据库模型
|
||||
*/
|
||||
export class Theme {
|
||||
"id": number;
|
||||
"name": string;
|
||||
"type": ThemeType;
|
||||
"colors": ThemeColorConfig;
|
||||
"isDefault": boolean;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = 0;
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = ("" as ThemeType);
|
||||
}
|
||||
if (!("colors" in $$source)) {
|
||||
this["colors"] = (new ThemeColorConfig());
|
||||
}
|
||||
if (!("isDefault" in $$source)) {
|
||||
this["isDefault"] = false;
|
||||
}
|
||||
if (!("createdAt" in $$source)) {
|
||||
this["createdAt"] = "";
|
||||
}
|
||||
if (!("updatedAt" in $$source)) {
|
||||
this["updatedAt"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Theme instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Theme {
|
||||
const $$createField3_0 = $$createType9;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("colors" in $$parsedSource) {
|
||||
$$parsedSource["colors"] = $$createField3_0($$parsedSource["colors"]);
|
||||
}
|
||||
return new Theme($$parsedSource as Partial<Theme>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemeColorConfig 主题颜色配置(与前端 ThemeColors 接口保持一致)
|
||||
*/
|
||||
export class ThemeColorConfig {
|
||||
/**
|
||||
* 主题基本信息
|
||||
* 主题名称
|
||||
*/
|
||||
"name": string;
|
||||
|
||||
/**
|
||||
* 是否为深色主题
|
||||
*/
|
||||
"dark": boolean;
|
||||
|
||||
/**
|
||||
* 基础色调
|
||||
* 主背景色
|
||||
*/
|
||||
"background": string;
|
||||
|
||||
/**
|
||||
* 次要背景色(用于代码块交替背景)
|
||||
*/
|
||||
"backgroundSecondary": string;
|
||||
|
||||
/**
|
||||
* 面板背景
|
||||
*/
|
||||
"surface": string;
|
||||
|
||||
/**
|
||||
* 下拉菜单背景
|
||||
*/
|
||||
"dropdownBackground": string;
|
||||
|
||||
/**
|
||||
* 下拉菜单边框
|
||||
*/
|
||||
"dropdownBorder": string;
|
||||
|
||||
/**
|
||||
* 文本颜色
|
||||
* 主文本色
|
||||
*/
|
||||
"foreground": string;
|
||||
|
||||
/**
|
||||
* 次要文本色
|
||||
*/
|
||||
"foregroundSecondary": string;
|
||||
|
||||
/**
|
||||
* 注释色
|
||||
*/
|
||||
"comment": string;
|
||||
|
||||
/**
|
||||
* 语法高亮色 - 核心
|
||||
* 关键字
|
||||
*/
|
||||
"keyword": string;
|
||||
|
||||
/**
|
||||
* 字符串
|
||||
*/
|
||||
"string": string;
|
||||
|
||||
/**
|
||||
* 函数名
|
||||
*/
|
||||
"function": string;
|
||||
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
"number": string;
|
||||
|
||||
/**
|
||||
* 操作符
|
||||
*/
|
||||
"operator": string;
|
||||
|
||||
/**
|
||||
* 变量
|
||||
*/
|
||||
"variable": string;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
"type": string;
|
||||
|
||||
/**
|
||||
* 语法高亮色 - 扩展
|
||||
* 常量
|
||||
*/
|
||||
"constant": string;
|
||||
|
||||
/**
|
||||
* 存储类型(如 static, const)
|
||||
*/
|
||||
"storage": string;
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
"parameter": string;
|
||||
|
||||
/**
|
||||
* 类名
|
||||
*/
|
||||
"class": string;
|
||||
|
||||
/**
|
||||
* 标题(Markdown等)
|
||||
*/
|
||||
"heading": string;
|
||||
|
||||
/**
|
||||
* 无效内容/错误
|
||||
*/
|
||||
"invalid": string;
|
||||
|
||||
/**
|
||||
* 正则表达式
|
||||
*/
|
||||
"regexp": string;
|
||||
|
||||
/**
|
||||
* 界面元素
|
||||
* 光标
|
||||
*/
|
||||
"cursor": string;
|
||||
|
||||
/**
|
||||
* 选中背景
|
||||
*/
|
||||
"selection": string;
|
||||
|
||||
/**
|
||||
* 失焦选中背景
|
||||
*/
|
||||
"selectionBlur": string;
|
||||
|
||||
/**
|
||||
* 当前行高亮
|
||||
*/
|
||||
"activeLine": string;
|
||||
|
||||
/**
|
||||
* 行号
|
||||
*/
|
||||
"lineNumber": string;
|
||||
|
||||
/**
|
||||
* 活动行号颜色
|
||||
*/
|
||||
"activeLineNumber": string;
|
||||
|
||||
/**
|
||||
* 边框和分割线
|
||||
* 边框色
|
||||
*/
|
||||
"borderColor": string;
|
||||
|
||||
/**
|
||||
* 浅色边框
|
||||
*/
|
||||
"borderLight": string;
|
||||
|
||||
/**
|
||||
* 搜索和匹配
|
||||
* 搜索匹配
|
||||
*/
|
||||
"searchMatch": string;
|
||||
|
||||
/**
|
||||
* 匹配括号
|
||||
*/
|
||||
"matchingBracket": string;
|
||||
|
||||
/** Creates a new ThemeColorConfig instance. */
|
||||
constructor($$source: Partial<ThemeColorConfig> = {}) {
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("dark" in $$source)) {
|
||||
this["dark"] = false;
|
||||
}
|
||||
if (!("background" in $$source)) {
|
||||
this["background"] = "";
|
||||
}
|
||||
if (!("backgroundSecondary" in $$source)) {
|
||||
this["backgroundSecondary"] = "";
|
||||
}
|
||||
if (!("surface" in $$source)) {
|
||||
this["surface"] = "";
|
||||
}
|
||||
if (!("dropdownBackground" in $$source)) {
|
||||
this["dropdownBackground"] = "";
|
||||
}
|
||||
if (!("dropdownBorder" in $$source)) {
|
||||
this["dropdownBorder"] = "";
|
||||
}
|
||||
if (!("foreground" in $$source)) {
|
||||
this["foreground"] = "";
|
||||
}
|
||||
if (!("foregroundSecondary" in $$source)) {
|
||||
this["foregroundSecondary"] = "";
|
||||
}
|
||||
if (!("comment" in $$source)) {
|
||||
this["comment"] = "";
|
||||
}
|
||||
if (!("keyword" in $$source)) {
|
||||
this["keyword"] = "";
|
||||
}
|
||||
if (!("string" in $$source)) {
|
||||
this["string"] = "";
|
||||
}
|
||||
if (!("function" in $$source)) {
|
||||
this["function"] = "";
|
||||
}
|
||||
if (!("number" in $$source)) {
|
||||
this["number"] = "";
|
||||
}
|
||||
if (!("operator" in $$source)) {
|
||||
this["operator"] = "";
|
||||
}
|
||||
if (!("variable" in $$source)) {
|
||||
this["variable"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = "";
|
||||
}
|
||||
if (!("constant" in $$source)) {
|
||||
this["constant"] = "";
|
||||
}
|
||||
if (!("storage" in $$source)) {
|
||||
this["storage"] = "";
|
||||
}
|
||||
if (!("parameter" in $$source)) {
|
||||
this["parameter"] = "";
|
||||
}
|
||||
if (!("class" in $$source)) {
|
||||
this["class"] = "";
|
||||
}
|
||||
if (!("heading" in $$source)) {
|
||||
this["heading"] = "";
|
||||
}
|
||||
if (!("invalid" in $$source)) {
|
||||
this["invalid"] = "";
|
||||
}
|
||||
if (!("regexp" in $$source)) {
|
||||
this["regexp"] = "";
|
||||
}
|
||||
if (!("cursor" in $$source)) {
|
||||
this["cursor"] = "";
|
||||
}
|
||||
if (!("selection" in $$source)) {
|
||||
this["selection"] = "";
|
||||
}
|
||||
if (!("selectionBlur" in $$source)) {
|
||||
this["selectionBlur"] = "";
|
||||
}
|
||||
if (!("activeLine" in $$source)) {
|
||||
this["activeLine"] = "";
|
||||
}
|
||||
if (!("lineNumber" in $$source)) {
|
||||
this["lineNumber"] = "";
|
||||
}
|
||||
if (!("activeLineNumber" in $$source)) {
|
||||
this["activeLineNumber"] = "";
|
||||
}
|
||||
if (!("borderColor" in $$source)) {
|
||||
this["borderColor"] = "";
|
||||
}
|
||||
if (!("borderLight" in $$source)) {
|
||||
this["borderLight"] = "";
|
||||
}
|
||||
if (!("searchMatch" in $$source)) {
|
||||
this["searchMatch"] = "";
|
||||
}
|
||||
if (!("matchingBracket" in $$source)) {
|
||||
this["matchingBracket"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ThemeColorConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): ThemeColorConfig {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemeType 主题类型枚举
|
||||
*/
|
||||
export enum ThemeType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
ThemeTypeDark = "dark",
|
||||
ThemeTypeLight = "light",
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateSourceType 更新源类型
|
||||
*/
|
||||
@@ -1608,8 +1134,8 @@ export class UpdatesConfig {
|
||||
* Creates a new UpdatesConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): UpdatesConfig {
|
||||
const $$createField6_0 = $$createType10;
|
||||
const $$createField7_0 = $$createType11;
|
||||
const $$createField6_0 = $$createType9;
|
||||
const $$createField7_0 = $$createType10;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("github" in $$parsedSource) {
|
||||
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
|
||||
@@ -1636,6 +1162,5 @@ var $$createType6 = (function $$initCreateType6(...args): any {
|
||||
});
|
||||
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType8 = HotkeyCombo.createFrom;
|
||||
const $$createType9 = ThemeColorConfig.createFrom;
|
||||
const $$createType10 = GithubConfig.createFrom;
|
||||
const $$createType11 = GiteaConfig.createFrom;
|
||||
const $$createType9 = GithubConfig.createFrom;
|
||||
const $$createType10 = GiteaConfig.createFrom;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* BackupService 提供基于Git的备份功能
|
||||
* BackupService 提供基于Git的备份同步功能
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* HandleConfigChange 处理备份配置变更
|
||||
* HandleConfigChange 处理配置变更
|
||||
*/
|
||||
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(395287784, config) as any;
|
||||
@@ -34,15 +34,7 @@ export function Initialize(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* PushToRemote 推送本地更改到远程仓库
|
||||
*/
|
||||
export function PushToRemote(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(262644139) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize 重新初始化备份服务,用于响应配置变更
|
||||
* Reinitialize 重新初始化
|
||||
*/
|
||||
export function Reinitialize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(301562543) as any;
|
||||
@@ -50,7 +42,7 @@ export function Reinitialize(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭时的清理工作
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(422131801) as any;
|
||||
@@ -63,7 +55,7 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* StartAutoBackup 启动自动备份定时器
|
||||
* StartAutoBackup 启动自动备份
|
||||
*/
|
||||
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3035755449) as any;
|
||||
@@ -77,3 +69,11 @@ export function StopAutoBackup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2641894021) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入
|
||||
*/
|
||||
export function Sync(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3420383211) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
import * as helper$0 from "../common/helper/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* Get 获取配置项
|
||||
@@ -50,7 +49,7 @@ export function MigrateConfig(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetConfig 强制重置所有配置为默认值
|
||||
* ResetConfig 重置所有配置为默认值
|
||||
*/
|
||||
export function ResetConfig(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3593047389) as any;
|
||||
@@ -66,7 +65,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3311949428, options) as any;
|
||||
@@ -84,7 +83,7 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
||||
/**
|
||||
* Watch 注册配置变更监听器
|
||||
*/
|
||||
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
|
||||
export function Watch(path: string, callback: helper$0.ObserverCallback): Promise<helper$0.CancelFunc> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -92,7 +91,7 @@ export function Watch(path: string, callback: $models.ObserverCallback): Promise
|
||||
/**
|
||||
* WatchWithContext 使用 Context 注册监听器
|
||||
*/
|
||||
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DatabaseService provides shared database functionality
|
||||
* DatabaseService 数据库服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -15,23 +15,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* OnDataPathChanged handles data path changes
|
||||
*/
|
||||
export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3652863491) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterModel 注册模型与表的映射关系
|
||||
*/
|
||||
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown shuts down the service when the application closes
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3907893632) as any;
|
||||
@@ -39,7 +23,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2067840771, options) as any;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DocumentService provides document management functionality
|
||||
* DocumentService 文档服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -15,12 +15,12 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* CreateDocument creates a new document and returns the created document with ID
|
||||
* CreateDocument 创建文档
|
||||
*/
|
||||
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
|
||||
export function CreateDocument(title: string): Promise<ent$0.Document | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3360680842, title) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
@@ -30,7 +30,7 @@ export function CreateDocument(title: string): Promise<models$0.Document | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
||||
* DeleteDocument 删除文档
|
||||
*/
|
||||
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(412287269, id) as any;
|
||||
@@ -38,9 +38,9 @@ export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDocumentByID gets a document by ID
|
||||
* GetDocumentByID 根据ID获取文档
|
||||
*/
|
||||
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
|
||||
export function GetDocumentByID(id: number): Promise<ent$0.Document | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3468193232, id) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
@@ -50,9 +50,9 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
|
||||
}
|
||||
|
||||
/**
|
||||
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
||||
* ListAllDocumentsMeta 列出所有文档
|
||||
*/
|
||||
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
|
||||
export function ListAllDocumentsMeta(): Promise<(ent$0.Document | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3073950297) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
@@ -62,19 +62,7 @@ export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> &
|
||||
}
|
||||
|
||||
/**
|
||||
* ListDeletedDocumentsMeta lists all deleted document metadata
|
||||
*/
|
||||
export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(490143787) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* LockDocument 锁定文档,防止删除
|
||||
* LockDocument 锁定文档
|
||||
*/
|
||||
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1889494473, id) as any;
|
||||
@@ -82,15 +70,7 @@ export function LockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* RestoreDocument restores a deleted document
|
||||
*/
|
||||
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(784200778, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1474135487, options) as any;
|
||||
@@ -106,7 +86,7 @@ export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentContent updates the content of a document
|
||||
* UpdateDocumentContent 更新文档内容
|
||||
*/
|
||||
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
|
||||
@@ -114,7 +94,7 @@ export function UpdateDocumentContent(id: number, content: string): Promise<void
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentTitle updates the title of a document
|
||||
* UpdateDocumentTitle 更新文档标题
|
||||
*/
|
||||
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
|
||||
@@ -122,6 +102,6 @@ export function UpdateDocumentTitle(id: number, title: string): Promise<void> &
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Document.createFrom;
|
||||
const $$createType0 = ent$0.Document.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* ExtensionService 扩展管理服务
|
||||
* ExtensionService 扩展服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -16,12 +16,39 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllExtensions 获取所有扩展配置
|
||||
* GetAllExtensions 获取所有扩展
|
||||
*/
|
||||
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
|
||||
export function GetAllExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3094292124) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
|
||||
*/
|
||||
export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4036328166) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetExtensionByKey 根据Key获取扩展
|
||||
*/
|
||||
export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2551065776, key) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -30,23 +57,15 @@ export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel():
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetAllExtensionsToDefault 重置所有扩展到默认状态
|
||||
* ResetExtensionConfig 重置单个扩展到默认状态
|
||||
*/
|
||||
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(270611949) as any;
|
||||
export function ResetExtensionConfig(key: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3990780299, key) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetExtensionToDefault 重置扩展到默认状态
|
||||
*/
|
||||
export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(868308101, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 启动时调用
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(40324057, options) as any;
|
||||
@@ -54,21 +73,32 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionEnabled 更新扩展启用状态
|
||||
* SyncExtensions 同步扩展配置
|
||||
*/
|
||||
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
|
||||
export function SyncExtensions(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(167560004) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionState 更新扩展状态
|
||||
* UpdateExtensionConfig 更新扩展配置
|
||||
*/
|
||||
export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any;
|
||||
export function UpdateExtensionConfig(key: string, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3184142503, key, config) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateExtensionEnabled 更新扩展启用状态
|
||||
*/
|
||||
export function UpdateExtensionEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1067300094, key, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Extension.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType0 = ent$0.Extension.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
const $$createType3 = models$0.Extension.createFrom;
|
||||
const $$createType4 = $Create.Array($$createType3);
|
||||
|
||||
@@ -18,12 +18,12 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* GetCurrentHotkey 获取当前热键
|
||||
* GetSupportedKeys 返回系统支持的快捷键列表
|
||||
*/
|
||||
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2572811187) as any;
|
||||
export function GetSupportedKeys(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1511528650) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
@@ -86,5 +86,4 @@ export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.HotkeyCombo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
@@ -17,7 +17,6 @@ import * as SystemService from "./systemservice.js";
|
||||
import * as TestService from "./testservice.js";
|
||||
import * as ThemeService from "./themeservice.js";
|
||||
import * as TranslationService from "./translationservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as WindowService from "./windowservice.js";
|
||||
export {
|
||||
BackupService,
|
||||
@@ -36,7 +35,6 @@ export {
|
||||
TestService,
|
||||
ThemeService,
|
||||
TranslationService,
|
||||
TrayService,
|
||||
WindowService
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* KeyBindingService 快捷键管理服务
|
||||
* KeyBindingService 快捷键服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -16,12 +16,39 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllKeyBindings 获取所有快捷键配置
|
||||
* GetAllKeyBindings 获取所有快捷键
|
||||
*/
|
||||
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
export function GetAllKeyBindings(): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1633502882) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultKeyBindings 获取默认快捷键配置
|
||||
*/
|
||||
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3843471588) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType4($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetKeyBindingByKey 根据Key获取快捷键
|
||||
*/
|
||||
export function GetKeyBindingByKey(key: string): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(852938650, key) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -30,13 +57,40 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 启动时调用
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2057121990, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncKeyBindings 同步快捷键配置
|
||||
*/
|
||||
export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1522202638) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingCommand 更新快捷键命令
|
||||
*/
|
||||
export function UpdateKeyBindingCommand(key: string, command: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1293670628, key, command) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateKeyBindingEnabled 更新快捷键启用状态
|
||||
*/
|
||||
export function UpdateKeyBindingEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(843626124, key, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.KeyBinding.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType0 = ent$0.KeyBinding.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
const $$createType3 = models$0.KeyBinding.createFrom;
|
||||
const $$createType4 = $Create.Array($$createType3);
|
||||
|
||||
@@ -12,12 +12,6 @@ import * as http$0 from "../../../net/http/models.js";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* HttpRequest HTTP请求结构
|
||||
*/
|
||||
@@ -191,15 +185,14 @@ export class MemoryStats {
|
||||
* MigrationProgress 迁移进度信息
|
||||
*/
|
||||
export class MigrationProgress {
|
||||
"status": MigrationStatus;
|
||||
/**
|
||||
* 0-100
|
||||
*/
|
||||
"progress": number;
|
||||
"error"?: string;
|
||||
|
||||
/** Creates a new MigrationProgress instance. */
|
||||
constructor($$source: Partial<MigrationProgress> = {}) {
|
||||
if (!("status" in $$source)) {
|
||||
this["status"] = ("" as MigrationStatus);
|
||||
}
|
||||
if (!("progress" in $$source)) {
|
||||
this["progress"] = 0;
|
||||
}
|
||||
@@ -216,20 +209,6 @@ export class MigrationProgress {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrationStatus 迁移状态
|
||||
*/
|
||||
export enum MigrationStatus {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
MigrationStatusMigrating = "migrating",
|
||||
MigrationStatusCompleted = "completed",
|
||||
MigrationStatusFailed = "failed",
|
||||
};
|
||||
|
||||
/**
|
||||
* OSInfo 操作系统信息
|
||||
*/
|
||||
@@ -266,11 +245,6 @@ export class OSInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
|
||||
/**
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
|
||||
@@ -15,26 +15,13 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
import * as ent$0 from "../models/ent/models.js";
|
||||
|
||||
/**
|
||||
* GetAllThemes 获取所有主题
|
||||
* GetThemeByKey 根据Key获取主题
|
||||
*/
|
||||
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2425053076) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThemeByID 根据ID或名称获取主题
|
||||
* 如果 id > 0,按ID查询;如果 id = 0,按名称查询
|
||||
*/
|
||||
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(127385338, id, name) as any;
|
||||
export function GetThemeByKey(key: string): Promise<ent$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(808794256, key) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
@@ -43,23 +30,15 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetTheme 重置主题为预设配置
|
||||
* ResetTheme 删除主题
|
||||
*/
|
||||
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1806334457, id, name) as any;
|
||||
export function ResetTheme(key: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1806334457, key) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1676749034) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动时初始化
|
||||
* ServiceStartup 服务启动
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2915959937, options) as any;
|
||||
@@ -67,14 +46,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateTheme 更新主题
|
||||
* UpdateTheme 保存或更新主题
|
||||
*/
|
||||
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(70189749, id, colors) as any;
|
||||
export function UpdateTheme(key: string, colors: { [_: string]: any }): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(70189749, key, colors) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Theme.createFrom;
|
||||
const $$createType0 = ent$0.Theme.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* TrayService 系统托盘服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* AutoShowHide 自动显示/隐藏主窗口
|
||||
*/
|
||||
export function AutoShowHide(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4044219428) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandleWindowClose 处理窗口关闭事件
|
||||
*/
|
||||
export function HandleWindowClose(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1824247204) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandleWindowMinimize 处理窗口最小化事件
|
||||
*/
|
||||
export function HandleWindowMinimize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(178686624) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* MinimizeButtonClicked 处理标题栏最小化按钮点击
|
||||
*/
|
||||
export function MinimizeButtonClicked(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2477618539) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShouldMinimizeToTray 检查是否应该最小化到托盘
|
||||
*/
|
||||
export function ShouldMinimizeToTray(): Promise<boolean> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3403884012) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShowWindow 显示主窗口
|
||||
*/
|
||||
export function ShowWindow(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1315913255) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -14,18 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* GetOpenWindows 获取所有打开的文档窗口
|
||||
*/
|
||||
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1464997251) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||
*/
|
||||
@@ -57,6 +45,3 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
2668
frontend/package-lock.json
generated
2668
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,8 +22,8 @@
|
||||
"app:generate": "cd .. && wails3 generate bindings -ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.19.1",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-lezer": "^6.0.2",
|
||||
"@codemirror/lang-liquid": "^6.3.0",
|
||||
"@codemirror/lang-liquid": "^6.3.1",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
@@ -47,56 +47,61 @@
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/legacy-modes": "^6.5.2",
|
||||
"@codemirror/lint": "^6.9.1",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@codemirror/view": "^6.39.4",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@lezer/lr": "^1.4.5",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@toml-tools/lexer": "^1.0.0",
|
||||
"@toml-tools/parser": "^1.0.0",
|
||||
"@types/katex": "^0.16.7",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"colors-named": "^1.0.4",
|
||||
"colors-named-hex": "^1.0.3",
|
||||
"groovy-beautify": "^0.0.17",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
"jsox": "^1.2.123",
|
||||
"linguist-languages": "^9.1.0",
|
||||
"katex": "^0.16.27",
|
||||
"linguist-languages": "^9.1.11",
|
||||
"marked": "^17.0.1",
|
||||
"mermaid": "^11.12.2",
|
||||
"php-parser": "^3.2.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.93.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.97.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.76",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.0",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"globals": "^16.5.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.12",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vitest": "^4.0.6",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.2"
|
||||
"vue-tsc": "^3.1.8"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@latest"
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ onBeforeMount(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
systemStore.initializeSystemInfo(),
|
||||
systemStore.initSystemInfo(),
|
||||
keybindingStore.loadKeyBindings(),
|
||||
]);
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
await themeStore.initializeTheme();
|
||||
await configStore.initLanguage();
|
||||
await themeStore.initTheme();
|
||||
await translationStore.loadTranslators();
|
||||
|
||||
// 启动时检查更新
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
鸿蒙字体压缩工具
|
||||
使用 fonttools 库压缩 TTF 字体文件,减小文件大小
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
def check_dependencies():
|
||||
"""检查必要的依赖是否已安装"""
|
||||
missing_packages = []
|
||||
|
||||
# 检查 fonttools
|
||||
try:
|
||||
import fontTools
|
||||
except ImportError:
|
||||
missing_packages.append('fonttools')
|
||||
|
||||
# 检查 brotli
|
||||
try:
|
||||
import brotli
|
||||
except ImportError:
|
||||
missing_packages.append('brotli')
|
||||
|
||||
# 检查 pyftsubset 命令是否可用
|
||||
try:
|
||||
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
missing_packages.append('fonttools[subset]')
|
||||
except FileNotFoundError:
|
||||
if 'fonttools' not in missing_packages:
|
||||
missing_packages.append('fonttools[subset]')
|
||||
|
||||
if missing_packages:
|
||||
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
|
||||
print("请运行以下命令安装:")
|
||||
print(f"pip install {' '.join(missing_packages)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_file_size(file_path: str) -> int:
|
||||
"""获取文件大小(字节)"""
|
||||
return os.path.getsize(file_path)
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""格式化文件大小显示"""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.2f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||||
|
||||
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
|
||||
"""
|
||||
压缩单个字体文件
|
||||
|
||||
Args:
|
||||
input_path: 输入字体文件路径
|
||||
output_path: 输出字体文件路径
|
||||
compression_level: 压缩级别 ("basic", "medium", "aggressive")
|
||||
|
||||
Returns:
|
||||
bool: 压缩是否成功
|
||||
"""
|
||||
try:
|
||||
# 基础压缩参数
|
||||
base_args = [
|
||||
"pyftsubset", input_path,
|
||||
"--output-file=" + output_path,
|
||||
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
|
||||
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
|
||||
]
|
||||
|
||||
# 根据压缩级别设置不同的参数
|
||||
if compression_level == "basic":
|
||||
# 基础压缩:保留常用字符和功能
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
|
||||
"--layout-features=*", # 保留所有布局特性
|
||||
"--glyph-names", # 保留字形名称
|
||||
"--symbol-cmap", # 保留符号映射
|
||||
"--legacy-cmap", # 保留传统字符映射
|
||||
"--notdef-glyph", # 保留 .notdef 字形
|
||||
"--recommended-glyphs", # 保留推荐字形
|
||||
"--name-IDs=*", # 保留所有名称ID
|
||||
"--name-legacy", # 保留传统名称
|
||||
]
|
||||
elif compression_level == "medium":
|
||||
# 中等压缩:移除一些不常用的功能
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
|
||||
"--layout-features=kern,liga,clig", # 只保留关键布局特性
|
||||
"--no-glyph-names", # 移除字形名称
|
||||
"--notdef-glyph",
|
||||
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
|
||||
]
|
||||
else: # aggressive
|
||||
# 激进压缩:最大程度减小文件大小
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
|
||||
"--no-layout-features", # 移除所有布局特性
|
||||
"--no-glyph-names", # 移除字形名称
|
||||
"--no-symbol-cmap", # 移除符号映射
|
||||
"--no-legacy-cmap", # 移除传统映射
|
||||
"--notdef-glyph",
|
||||
"--name-IDs=1,2", # 只保留最基本的名称
|
||||
"--desubroutinize", # 去子程序化(可能减小CFF字体大小)
|
||||
]
|
||||
|
||||
# 执行压缩命令
|
||||
result = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"压缩失败: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"压缩过程中出现错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def find_font_files(directory: str) -> List[str]:
|
||||
"""查找目录中的所有字体文件"""
|
||||
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
|
||||
font_files = []
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if any(file.lower().endswith(ext) for ext in font_extensions):
|
||||
font_files.append(os.path.join(root, file))
|
||||
|
||||
return font_files
|
||||
|
||||
def compress_fonts_batch(font_directory: str, compression_level: str = "basic"):
|
||||
"""
|
||||
批量压缩字体文件
|
||||
|
||||
Args:
|
||||
font_directory: 字体文件目录
|
||||
compression_level: 压缩级别
|
||||
"""
|
||||
if not os.path.exists(font_directory):
|
||||
print(f"错误: 目录 {font_directory} 不存在")
|
||||
return
|
||||
|
||||
# 查找所有字体文件
|
||||
font_files = find_font_files(font_directory)
|
||||
|
||||
if not font_files:
|
||||
print("未找到字体文件")
|
||||
return
|
||||
|
||||
print(f"找到 {len(font_files)} 个字体文件")
|
||||
print(f"压缩级别: {compression_level}")
|
||||
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
|
||||
print("-" * 60)
|
||||
|
||||
total_original_size = 0
|
||||
total_compressed_size = 0
|
||||
successful_compressions = 0
|
||||
|
||||
for i, font_file in enumerate(font_files, 1):
|
||||
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
|
||||
|
||||
# 获取原始文件大小
|
||||
original_size = get_file_size(font_file)
|
||||
total_original_size += original_size
|
||||
|
||||
# 生成输出文件名(保持原文件名,只改变扩展名)
|
||||
file_dir = os.path.dirname(font_file)
|
||||
base_name = os.path.splitext(os.path.basename(font_file))[0]
|
||||
output_file = os.path.join(file_dir, f"{base_name}.woff2")
|
||||
|
||||
# 压缩字体
|
||||
if compress_font(font_file, output_file, compression_level):
|
||||
if os.path.exists(output_file):
|
||||
compressed_size = get_file_size(output_file)
|
||||
total_compressed_size += compressed_size
|
||||
successful_compressions += 1
|
||||
|
||||
# 计算压缩率
|
||||
compression_ratio = (1 - compressed_size / original_size) * 100
|
||||
|
||||
print(f" ✓ 成功: {format_file_size(original_size)} → {format_file_size(compressed_size)} "
|
||||
f"(压缩 {compression_ratio:.1f}%)")
|
||||
else:
|
||||
print(f" ✗ 失败: 输出文件未生成")
|
||||
else:
|
||||
print(f" ✗ 失败: 压缩过程出错")
|
||||
|
||||
print()
|
||||
|
||||
# 显示总结
|
||||
print("=" * 60)
|
||||
print("压缩完成!")
|
||||
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
|
||||
|
||||
if successful_compressions > 0:
|
||||
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
|
||||
print(f"总大小: {format_file_size(total_original_size)} → {format_file_size(total_compressed_size)}")
|
||||
print(f"总压缩率: {total_compression_ratio:.1f}%")
|
||||
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("鸿蒙字体压缩工具")
|
||||
print("=" * 60)
|
||||
|
||||
# 检查依赖
|
||||
if not check_dependencies():
|
||||
return
|
||||
|
||||
# 获取当前脚本所在目录
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 设置默认字体目录
|
||||
font_directory = current_dir
|
||||
|
||||
print(f"字体目录: {font_directory}")
|
||||
|
||||
# 让用户选择压缩级别
|
||||
print("\n请选择压缩级别:")
|
||||
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
|
||||
print("2. 中等压缩 (平衡文件大小和功能)")
|
||||
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
|
||||
|
||||
while True:
|
||||
choice = input("\n请输入选择 (1-3): ").strip()
|
||||
if choice == "1":
|
||||
compression_level = "basic"
|
||||
break
|
||||
elif choice == "2":
|
||||
compression_level = "medium"
|
||||
break
|
||||
elif choice == "3":
|
||||
compression_level = "aggressive"
|
||||
break
|
||||
else:
|
||||
print("无效选择,请输入 1、2 或 3")
|
||||
|
||||
# 开始批量压缩
|
||||
compress_fonts_batch(font_directory, compression_level=compression_level)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Black.otf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Black.otf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Black.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Black.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Bold.otf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Bold.otf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Bold.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-ExtraLight.otf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-ExtraLight.otf
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Italic.otf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Italic.otf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Italic.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Italic.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Light.otf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Light.otf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Light.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-Light.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-SemiBold.otf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-SemiBold.otf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-SemiBold.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/otf/Monocraft-SemiBold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Black.ttf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Black.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Black.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Black.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Bold.ttf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Bold.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Bold.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-ExtraLight.ttf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-ExtraLight.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Italic.ttf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Italic.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Italic.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Italic.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Light.ttf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Light.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Light.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-Light.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-SemiBold.ttf
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-SemiBold.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-SemiBold.woff2
Normal file
BIN
frontend/src/assets/fonts/Monocraft/ttf/Monocraft-SemiBold.woff2
Normal file
Binary file not shown.
179
frontend/src/assets/fonts/README.md
Normal file
179
frontend/src/assets/fonts/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 字体压缩工具使用指南
|
||||
|
||||
## 📖 简介
|
||||
|
||||
`font_compressor.py` 是一个通用的字体压缩工具,可以:
|
||||
- ✅ 将 TTF、OTF、WOFF 字体文件转换为 WOFF2 格式
|
||||
- ✅ 支持相对路径和绝对路径
|
||||
- ✅ 自动生成 CSS 字体定义文件
|
||||
- ✅ 智能识别字体字重和样式
|
||||
- ✅ 批量处理整个目录(包括子目录)
|
||||
|
||||
## 🚀 前置要求
|
||||
|
||||
安装 Python 依赖包:
|
||||
|
||||
```bash
|
||||
pip install fonttools brotli
|
||||
```
|
||||
|
||||
## 📝 使用方法
|
||||
|
||||
### 基础用法
|
||||
|
||||
```bash
|
||||
# 进入 fonts 目录
|
||||
cd frontend/src/assets/fonts
|
||||
|
||||
# 交互式模式处理当前目录
|
||||
python font_compressor.py
|
||||
|
||||
# 处理相对路径的 Monocraft 目录
|
||||
python font_compressor.py Monocraft
|
||||
|
||||
# 处理相对路径并指定压缩级别
|
||||
python font_compressor.py Monocraft -l basic
|
||||
```
|
||||
|
||||
### 生成 CSS 文件
|
||||
|
||||
```bash
|
||||
# 压缩 Monocraft 字体并生成 CSS 文件
|
||||
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
|
||||
|
||||
# 压缩 Hack 字体并生成 CSS
|
||||
python font_compressor.py Hack -l basic -c ../styles/hack_fonts_new.css
|
||||
|
||||
# 压缩 OpenSans 字体并生成 CSS
|
||||
python font_compressor.py OpenSans -l medium -c ../styles/opensans_fonts.css
|
||||
```
|
||||
|
||||
### 高级用法
|
||||
|
||||
```bash
|
||||
# 使用绝对路径
|
||||
python font_compressor.py E:\Go_WorkSpace\voidraft\frontend\src\assets\fonts\Monocraft -l basic -c monocraft.css
|
||||
|
||||
# 不同压缩级别
|
||||
python font_compressor.py Monocraft -l basic # 基础压缩,保留所有功能
|
||||
python font_compressor.py Monocraft -l medium # 中等压缩,平衡大小和功能
|
||||
python font_compressor.py Monocraft -l aggressive # 激进压缩,最小文件
|
||||
```
|
||||
|
||||
## ⚙️ 命令行参数
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `directory` | 字体目录(相对/绝对路径) | `Monocraft` 或 `/path/to/fonts` |
|
||||
| `-l, --level` | 压缩级别 (basic/medium/aggressive) | `-l basic` |
|
||||
| `-c, --css` | CSS 输出文件路径 | `-c monocraft.css` |
|
||||
| `--version` | 显示版本信息 | `--version` |
|
||||
| `-h, --help` | 显示帮助信息 | `-h` |
|
||||
|
||||
## 📊 压缩级别说明
|
||||
|
||||
### basic(基础) - 推荐
|
||||
- 保留大部分字体功能
|
||||
- 适合网页使用
|
||||
- 压缩率约 30-40%
|
||||
|
||||
### medium(中等)
|
||||
- 移除一些不常用的功能
|
||||
- 平衡文件大小和功能
|
||||
- 压缩率约 40-50%
|
||||
|
||||
### aggressive(激进)
|
||||
- 最大程度减小文件大小
|
||||
- 可能影响高级排版功能
|
||||
- 压缩率约 50-60%
|
||||
|
||||
## 📁 输出结果
|
||||
|
||||
### 字体文件
|
||||
压缩后的 `.woff2` 文件会保存在原文件相同的目录下,例如:
|
||||
- `Monocraft/ttf/Monocraft-Bold.ttf` → `Monocraft/ttf/Monocraft-Bold.woff2`
|
||||
- `Hack/hack-regular.ttf` → `Hack/hack-regular.woff2`
|
||||
|
||||
### CSS 文件
|
||||
生成的 CSS 文件会包含:
|
||||
- 自动识别的字体家族名称
|
||||
- 正确的字重和样式设置
|
||||
- 使用相对路径的字体引用
|
||||
- 按字重排序的 `@font-face` 定义
|
||||
|
||||
生成的 CSS 示例:
|
||||
|
||||
```css
|
||||
/* 自动生成的字体文件 */
|
||||
/* 由 font_compressor.py 生成 */
|
||||
|
||||
/* Monocraft 字体家族 */
|
||||
|
||||
/* Monocraft Light */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Light.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Bold */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 实际使用示例
|
||||
|
||||
### 示例 1: 压缩 Monocraft 字体
|
||||
|
||||
```bash
|
||||
cd frontend/src/assets/fonts
|
||||
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
|
||||
```
|
||||
|
||||
这将:
|
||||
1. 扫描 `Monocraft/ttf` 和 `Monocraft/otf` 目录
|
||||
2. 将所有字体文件转换为 WOFF2
|
||||
3. 在 `frontend/src/assets/styles/monocraft_fonts.css` 生成 CSS 文件
|
||||
|
||||
### 示例 2: 批量处理多个字体目录
|
||||
|
||||
```bash
|
||||
cd frontend/src/assets/fonts
|
||||
|
||||
# 压缩 Monocraft
|
||||
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
|
||||
|
||||
# 压缩 OpenSans
|
||||
python font_compressor.py OpenSans -l basic -c ../styles/opensans_fonts.css
|
||||
|
||||
# 压缩 Hack(已有 CSS,只需生成新版本对比)
|
||||
python font_compressor.py Hack -l basic -c ../styles/hack_fonts_new.css
|
||||
```
|
||||
|
||||
## 🔍 字体信息自动识别
|
||||
|
||||
工具会自动从文件名识别:
|
||||
- **字重**:Thin(100), Light(300), Regular(400), Medium(500), SemiBold(600), Bold(700), Black(900)
|
||||
- **样式**:normal, italic
|
||||
- **字体家族**:自动去除字重和样式后缀
|
||||
|
||||
支持的命名格式:
|
||||
- `FontName-Bold.ttf`
|
||||
- `FontName_Bold.otf`
|
||||
- `FontName-BoldItalic.ttf`
|
||||
- `FontName_SemiBold_Italic.woff`
|
||||
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
```bash
|
||||
python font_compressor.py --help
|
||||
```
|
||||
|
||||
494
frontend/src/assets/fonts/font_compressor.py
Normal file
494
frontend/src/assets/fonts/font_compressor.py
Normal file
@@ -0,0 +1,494 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
通用字体压缩工具
|
||||
使用 fonttools 库将字体文件转换为 WOFF2 格式,减小文件大小
|
||||
支持 TTF、OTF、WOFF 等格式的字体文件
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
|
||||
def check_dependencies():
|
||||
"""检查必要的依赖是否已安装"""
|
||||
missing_packages = []
|
||||
|
||||
# 检查 fonttools
|
||||
try:
|
||||
import fontTools
|
||||
except ImportError:
|
||||
missing_packages.append('fonttools')
|
||||
|
||||
# 检查 brotli
|
||||
try:
|
||||
import brotli
|
||||
except ImportError:
|
||||
missing_packages.append('brotli')
|
||||
|
||||
# 检查 pyftsubset 命令是否可用
|
||||
try:
|
||||
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
missing_packages.append('fonttools[subset]')
|
||||
except FileNotFoundError:
|
||||
if 'fonttools' not in missing_packages:
|
||||
missing_packages.append('fonttools[subset]')
|
||||
|
||||
if missing_packages:
|
||||
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
|
||||
print("请运行以下命令安装:")
|
||||
print(f"pip install {' '.join(missing_packages)}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_file_size(file_path: str) -> int:
|
||||
"""获取文件大小(字节)"""
|
||||
return os.path.getsize(file_path)
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""格式化文件大小显示"""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.2f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||||
|
||||
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
|
||||
"""
|
||||
压缩单个字体文件
|
||||
|
||||
Args:
|
||||
input_path: 输入字体文件路径
|
||||
output_path: 输出字体文件路径
|
||||
compression_level: 压缩级别 ("basic", "medium", "aggressive")
|
||||
|
||||
Returns:
|
||||
bool: 压缩是否成功
|
||||
"""
|
||||
try:
|
||||
# 基础压缩参数
|
||||
base_args = [
|
||||
"pyftsubset", input_path,
|
||||
"--output-file=" + output_path,
|
||||
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
|
||||
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
|
||||
]
|
||||
|
||||
# 根据压缩级别设置不同的参数
|
||||
if compression_level == "basic":
|
||||
# 基础压缩:保留常用字符和功能
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
|
||||
"--layout-features=*", # 保留所有布局特性
|
||||
"--glyph-names", # 保留字形名称
|
||||
"--symbol-cmap", # 保留符号映射
|
||||
"--legacy-cmap", # 保留传统字符映射
|
||||
"--notdef-glyph", # 保留 .notdef 字形
|
||||
"--recommended-glyphs", # 保留推荐字形
|
||||
"--name-IDs=*", # 保留所有名称ID
|
||||
"--name-legacy", # 保留传统名称
|
||||
]
|
||||
elif compression_level == "medium":
|
||||
# 中等压缩:移除一些不常用的功能
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
|
||||
"--layout-features=kern,liga,clig", # 只保留关键布局特性
|
||||
"--no-glyph-names", # 移除字形名称
|
||||
"--notdef-glyph",
|
||||
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
|
||||
]
|
||||
else: # aggressive
|
||||
# 激进压缩:最大程度减小文件大小
|
||||
args = base_args + [
|
||||
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
|
||||
"--no-layout-features", # 移除所有布局特性
|
||||
"--no-glyph-names", # 移除字形名称
|
||||
"--no-symbol-cmap", # 移除符号映射
|
||||
"--no-legacy-cmap", # 移除传统映射
|
||||
"--notdef-glyph",
|
||||
"--name-IDs=1,2", # 只保留最基本的名称
|
||||
"--desubroutinize", # 去子程序化(可能减小CFF字体大小)
|
||||
]
|
||||
|
||||
# 执行压缩命令
|
||||
result = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
print(f"压缩失败: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"压缩过程中出现错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def find_font_files(directory: str, exclude_woff2: bool = False) -> List[str]:
|
||||
"""查找目录中的所有字体文件"""
|
||||
if exclude_woff2:
|
||||
font_extensions = ['.ttf', '.otf', '.woff']
|
||||
else:
|
||||
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
|
||||
font_files = []
|
||||
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
if any(file.lower().endswith(ext) for ext in font_extensions):
|
||||
font_files.append(os.path.join(root, file))
|
||||
|
||||
return font_files
|
||||
|
||||
def parse_font_info(filename: str) -> Dict[str, any]:
|
||||
"""
|
||||
从字体文件名解析字体信息(字重、样式等)
|
||||
|
||||
Args:
|
||||
filename: 字体文件名(不含路径)
|
||||
|
||||
Returns:
|
||||
包含字体信息的字典
|
||||
"""
|
||||
# 移除扩展名
|
||||
name_without_ext = os.path.splitext(filename)[0]
|
||||
|
||||
# 字重映射
|
||||
weight_mapping = {
|
||||
'thin': (100, 'Thin'),
|
||||
'extralight': (200, 'ExtraLight'),
|
||||
'light': (300, 'Light'),
|
||||
'regular': (400, 'Regular'),
|
||||
'normal': (400, 'Regular'),
|
||||
'medium': (500, 'Medium'),
|
||||
'semibold': (600, 'SemiBold'),
|
||||
'bold': (700, 'Bold'),
|
||||
'extrabold': (800, 'ExtraBold'),
|
||||
'black': (900, 'Black'),
|
||||
'heavy': (900, 'Heavy'),
|
||||
}
|
||||
|
||||
# 默认值
|
||||
font_weight = 400
|
||||
font_style = 'normal'
|
||||
weight_name = 'Regular'
|
||||
|
||||
# 检查是否为斜体
|
||||
if re.search(r'italic', name_without_ext, re.IGNORECASE):
|
||||
font_style = 'italic'
|
||||
|
||||
# 检查字重
|
||||
name_lower = name_without_ext.lower()
|
||||
for weight_key, (weight_value, weight_label) in weight_mapping.items():
|
||||
if weight_key in name_lower:
|
||||
font_weight = weight_value
|
||||
weight_name = weight_label
|
||||
break
|
||||
|
||||
# 提取字体家族名称(移除字重和样式后缀)
|
||||
family_name = name_without_ext
|
||||
for weight_key, (_, weight_label) in weight_mapping.items():
|
||||
family_name = re.sub(r'[-_]?' + weight_label, '', family_name, flags=re.IGNORECASE)
|
||||
family_name = re.sub(r'[-_]?italic', '', family_name, flags=re.IGNORECASE)
|
||||
family_name = family_name.strip('-_')
|
||||
|
||||
return {
|
||||
'family': family_name,
|
||||
'weight': font_weight,
|
||||
'style': font_style,
|
||||
'weight_name': weight_name,
|
||||
'full_name': name_without_ext
|
||||
}
|
||||
|
||||
def generate_css(font_files: List[str], output_css_path: str, css_base_path: str):
|
||||
"""
|
||||
生成CSS字体文件
|
||||
|
||||
Args:
|
||||
font_files: 字体文件路径列表(woff2文件)
|
||||
output_css_path: 输出CSS文件路径
|
||||
css_base_path: CSS文件相对于字体文件的基础路径
|
||||
"""
|
||||
# 按字体家族分组
|
||||
font_groups: Dict[str, List[Dict]] = {}
|
||||
|
||||
for font_file in font_files:
|
||||
if not font_file.endswith('.woff2'):
|
||||
continue
|
||||
|
||||
filename = os.path.basename(font_file)
|
||||
font_info = parse_font_info(filename)
|
||||
|
||||
# 计算相对路径
|
||||
font_dir = os.path.dirname(font_file)
|
||||
css_dir = os.path.dirname(output_css_path)
|
||||
|
||||
try:
|
||||
# 计算从CSS文件到字体文件的相对路径
|
||||
rel_path = os.path.relpath(font_file, css_dir)
|
||||
# 统一使用正斜杠(适用于Web)
|
||||
rel_path = rel_path.replace('\\', '/')
|
||||
except ValueError:
|
||||
# 如果在不同驱动器上,使用绝对路径
|
||||
rel_path = font_file.replace('\\', '/')
|
||||
|
||||
font_info['path'] = rel_path
|
||||
|
||||
family = font_info['family']
|
||||
if family not in font_groups:
|
||||
font_groups[family] = []
|
||||
font_groups[family].append(font_info)
|
||||
|
||||
# 生成CSS内容
|
||||
css_lines = ['/* 自动生成的字体文件 */', '/* 由 font_compressor.py 生成 */', '']
|
||||
|
||||
for family, fonts in sorted(font_groups.items()):
|
||||
css_lines.append(f'/* {family} 字体家族 */')
|
||||
css_lines.append('')
|
||||
|
||||
# 按字重排序
|
||||
fonts.sort(key=lambda x: (x['weight'], x['style']))
|
||||
|
||||
for font in fonts:
|
||||
css_lines.append(f"/* {family} {font['weight_name']}{' Italic' if font['style'] == 'italic' else ''} */")
|
||||
css_lines.append('@font-face {')
|
||||
css_lines.append(f" font-family: '{family}';")
|
||||
css_lines.append(f" src: url('{font['path']}') format('woff2');")
|
||||
css_lines.append(f" font-weight: {font['weight']};")
|
||||
css_lines.append(f" font-style: {font['style']};")
|
||||
css_lines.append(' font-display: swap;')
|
||||
css_lines.append('}')
|
||||
css_lines.append('')
|
||||
|
||||
# 写入CSS文件
|
||||
with open(output_css_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(css_lines))
|
||||
|
||||
print(f"[OK] CSS文件已生成: {output_css_path}")
|
||||
print(f" 包含 {sum(len(fonts) for fonts in font_groups.values())} 个字体定义")
|
||||
print(f" 字体家族: {', '.join(sorted(font_groups.keys()))}")
|
||||
|
||||
def compress_fonts_batch(font_directory: str, compression_level: str = "basic") -> List[str]:
|
||||
"""
|
||||
批量压缩字体文件
|
||||
|
||||
Args:
|
||||
font_directory: 字体文件目录
|
||||
compression_level: 压缩级别
|
||||
|
||||
Returns:
|
||||
生成的woff2文件路径列表
|
||||
"""
|
||||
if not os.path.exists(font_directory):
|
||||
print(f"错误: 目录 {font_directory} 不存在")
|
||||
return []
|
||||
|
||||
# 查找所有字体文件(排除已经是woff2的)
|
||||
font_files = find_font_files(font_directory, exclude_woff2=True)
|
||||
|
||||
if not font_files:
|
||||
print("未找到字体文件")
|
||||
return []
|
||||
|
||||
print(f"找到 {len(font_files)} 个字体文件")
|
||||
print(f"压缩级别: {compression_level}")
|
||||
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
|
||||
print("-" * 60)
|
||||
|
||||
total_original_size = 0
|
||||
total_compressed_size = 0
|
||||
successful_compressions = 0
|
||||
generated_woff2_files = []
|
||||
|
||||
for i, font_file in enumerate(font_files, 1):
|
||||
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
|
||||
|
||||
# 获取原始文件大小
|
||||
original_size = get_file_size(font_file)
|
||||
total_original_size += original_size
|
||||
|
||||
# 生成输出文件名(保持原文件名,只改变扩展名)
|
||||
file_dir = os.path.dirname(font_file)
|
||||
base_name = os.path.splitext(os.path.basename(font_file))[0]
|
||||
output_file = os.path.join(file_dir, f"{base_name}.woff2")
|
||||
|
||||
# 压缩字体
|
||||
if compress_font(font_file, output_file, compression_level):
|
||||
if os.path.exists(output_file):
|
||||
compressed_size = get_file_size(output_file)
|
||||
total_compressed_size += compressed_size
|
||||
successful_compressions += 1
|
||||
generated_woff2_files.append(output_file)
|
||||
|
||||
# 计算压缩率
|
||||
compression_ratio = (1 - compressed_size / original_size) * 100
|
||||
|
||||
print(f" [OK] 成功: {format_file_size(original_size)} -> {format_file_size(compressed_size)} "
|
||||
f"(压缩 {compression_ratio:.1f}%)")
|
||||
else:
|
||||
print(f" [失败] 输出文件未生成")
|
||||
else:
|
||||
print(f" [失败] 压缩过程出错")
|
||||
|
||||
print()
|
||||
|
||||
# 显示总结
|
||||
print("=" * 60)
|
||||
print("压缩完成!")
|
||||
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
|
||||
|
||||
if successful_compressions > 0:
|
||||
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
|
||||
print(f"总大小: {format_file_size(total_original_size)} → {format_file_size(total_compressed_size)}")
|
||||
print(f"总压缩率: {total_compression_ratio:.1f}%")
|
||||
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
|
||||
|
||||
return generated_woff2_files
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 解析命令行参数
|
||||
parser = argparse.ArgumentParser(
|
||||
description='通用字体压缩工具 - 将字体文件转换为 WOFF2 格式并生成CSS',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
使用示例:
|
||||
%(prog)s # 交互式模式,处理当前目录
|
||||
%(prog)s Monocraft # 处理相对路径目录
|
||||
%(prog)s Monocraft -l basic # 使用基础压缩级别
|
||||
%(prog)s Monocraft -l basic -c monocraft.css # 压缩并生成CSS文件
|
||||
%(prog)s /path/to/fonts -l medium -c fonts.css # 使用绝对路径
|
||||
|
||||
压缩级别说明:
|
||||
basic - 基础压缩:保留大部分功能,适合网页使用
|
||||
medium - 中等压缩:平衡文件大小和功能
|
||||
aggressive - 激进压缩:最小文件大小,可能影响显示效果
|
||||
|
||||
CSS生成说明:
|
||||
使用 -c/--css 选项生成CSS文件,自动使用相对路径引用字体文件
|
||||
'''
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'directory',
|
||||
nargs='?',
|
||||
default=None,
|
||||
help='字体文件目录路径(支持相对/绝对路径,默认为当前脚本所在目录)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-l', '--level',
|
||||
choices=['basic', 'medium', 'aggressive'],
|
||||
default=None,
|
||||
help='压缩级别:basic(基础)、medium(中等)、aggressive(激进)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-c', '--css',
|
||||
default=None,
|
||||
help='生成CSS文件路径(相对于脚本位置或绝对路径)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version='%(prog)s 2.0'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 60)
|
||||
print("通用字体压缩工具 v2.0")
|
||||
print("=" * 60)
|
||||
|
||||
# 检查依赖
|
||||
if not check_dependencies():
|
||||
return
|
||||
|
||||
# 获取脚本所在目录
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 确定字体目录
|
||||
if args.directory:
|
||||
# 支持相对路径和绝对路径
|
||||
if os.path.isabs(args.directory):
|
||||
font_directory = args.directory
|
||||
else:
|
||||
font_directory = os.path.join(script_dir, args.directory)
|
||||
font_directory = os.path.abspath(font_directory)
|
||||
else:
|
||||
# 默认使用当前脚本所在目录
|
||||
font_directory = script_dir
|
||||
|
||||
# 检查目录是否存在
|
||||
if not os.path.exists(font_directory):
|
||||
print(f"\n错误: 目录不存在: {font_directory}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n字体目录: {font_directory}")
|
||||
|
||||
# 确定压缩级别
|
||||
compression_level = args.level
|
||||
|
||||
if compression_level is None:
|
||||
# 交互式选择压缩级别
|
||||
print("\n请选择压缩级别:")
|
||||
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
|
||||
print("2. 中等压缩 (平衡文件大小和功能)")
|
||||
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
|
||||
|
||||
while True:
|
||||
choice = input("\n请输入选择 (1-3): ").strip()
|
||||
if choice == "1":
|
||||
compression_level = "basic"
|
||||
break
|
||||
elif choice == "2":
|
||||
compression_level = "medium"
|
||||
break
|
||||
elif choice == "3":
|
||||
compression_level = "aggressive"
|
||||
break
|
||||
else:
|
||||
print("无效选择,请输入 1、2 或 3")
|
||||
|
||||
# 开始批量压缩
|
||||
print()
|
||||
generated_files = compress_fonts_batch(font_directory, compression_level=compression_level)
|
||||
|
||||
# 生成CSS文件
|
||||
if args.css and generated_files:
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("生成CSS文件...")
|
||||
print("=" * 60)
|
||||
|
||||
# 确定CSS输出路径
|
||||
if os.path.isabs(args.css):
|
||||
css_path = args.css
|
||||
else:
|
||||
css_path = os.path.join(script_dir, args.css)
|
||||
css_path = os.path.abspath(css_path)
|
||||
|
||||
# 确保输出目录存在
|
||||
css_dir = os.path.dirname(css_path)
|
||||
if css_dir and not os.path.exists(css_dir):
|
||||
os.makedirs(css_dir)
|
||||
|
||||
# 生成CSS
|
||||
generate_css(generated_files, css_path, script_dir)
|
||||
elif args.css and not generated_files:
|
||||
print("\n警告: 没有成功生成WOFF2文件,跳过CSS生成")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("全部完成!")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,9 @@
|
||||
/* 导入所有CSS文件 */
|
||||
@import 'normalize.css';
|
||||
@import 'variables.css';
|
||||
@import "harmony_fonts.css";
|
||||
@import 'scrollbar.css';
|
||||
@import 'hack_fonts.css';
|
||||
@import 'opensans_fonts.css';
|
||||
@import "monocraft_fonts.css";
|
||||
@import 'variables.css';
|
||||
@import 'scrollbar.css';
|
||||
@import 'styles.css';
|
||||
202
frontend/src/assets/styles/monocraft_fonts.css
Normal file
202
frontend/src/assets/styles/monocraft_fonts.css
Normal file
@@ -0,0 +1,202 @@
|
||||
/* 自动生成的字体文件 */
|
||||
/* 由 font_compressor.py 生成 */
|
||||
|
||||
/* Monocraft 字体家族 */
|
||||
|
||||
/* Monocraft ExtraLight Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-ExtraLight-Italic.woff2') format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft ExtraLight Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-ExtraLight-Italic.woff2') format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft ExtraLight */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-ExtraLight.woff2') format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft ExtraLight */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-ExtraLight.woff2') format('woff2');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Light Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Light-Italic.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Light Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Light-Italic.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Light */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Light.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Light */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Light.woff2') format('woff2');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Regular Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Regular Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Italic.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft SemiBold Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-SemiBold-Italic.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft SemiBold Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-SemiBold-Italic.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft SemiBold */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-SemiBold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft SemiBold */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-SemiBold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Bold Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Bold-Italic.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Bold Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Bold-Italic.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Bold */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Bold */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Black Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Black-Italic.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Black Italic */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Black-Italic.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Black */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/otf/Monocraft-Black.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Monocraft Black */
|
||||
@font-face {
|
||||
font-family: 'Monocraft';
|
||||
src: url('../fonts/Monocraft/ttf/Monocraft-Black.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
3
frontend/src/assets/styles/styles.css
Normal file
3
frontend/src/assets/styles/styles.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
@@ -1,255 +1,266 @@
|
||||
:root {
|
||||
/* 编辑器区域 */
|
||||
--text-primary: #9BB586; /* 内容区域字体颜色 */
|
||||
|
||||
/* 深色主题颜色变量 */
|
||||
--dark-toolbar-bg: #2d2d2d;
|
||||
--dark-toolbar-border: #404040;
|
||||
--dark-toolbar-text: #ffffff;
|
||||
--dark-toolbar-text-secondary: #cccccc;
|
||||
--dark-toolbar-button-hover: #404040;
|
||||
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
|
||||
--dark-bg-secondary: #0E1217;
|
||||
--dark-text-secondary: #a0aec0;
|
||||
--dark-text-muted: #666;
|
||||
--dark-border-color: #2d3748;
|
||||
--dark-settings-bg: #2a2a2a;
|
||||
--dark-settings-card-bg: #333333;
|
||||
--dark-settings-text: #ffffff;
|
||||
--dark-settings-text-secondary: #cccccc;
|
||||
--dark-settings-border: #444444;
|
||||
--dark-settings-input-bg: #3a3a3a;
|
||||
--dark-settings-input-border: #555555;
|
||||
--dark-settings-hover: #404040;
|
||||
--dark-scrollbar-track: #2a2a2a;
|
||||
--dark-scrollbar-thumb: #555555;
|
||||
--dark-scrollbar-thumb-hover: #666666;
|
||||
--dark-selection-bg: rgba(181, 206, 168, 0.1);
|
||||
--dark-selection-text: #b5cea8;
|
||||
--dark-danger-color: #ff6b6b;
|
||||
--dark-bg-primary: #1a1a1a;
|
||||
--dark-bg-hover: #2a2a2a;
|
||||
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
|
||||
--dark-loading-color: #fff;
|
||||
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
|
||||
--dark-loading-done-color: #6f6;
|
||||
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
|
||||
|
||||
/* 浅色主题颜色变量 */
|
||||
--light-toolbar-bg: #f8f9fa;
|
||||
--light-toolbar-border: #e9ecef;
|
||||
--light-toolbar-text: #212529;
|
||||
--light-toolbar-text-secondary: #495057;
|
||||
--light-toolbar-button-hover: #e9ecef;
|
||||
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||
--light-bg-secondary: #f7fef7;
|
||||
--light-text-secondary: #374151;
|
||||
--light-text-muted: #6b7280;
|
||||
--light-border-color: #e5e7eb;
|
||||
--light-settings-bg: #ffffff;
|
||||
--light-settings-card-bg: #f8f9fa;
|
||||
--light-settings-text: #212529;
|
||||
--light-settings-text-secondary: #6c757d;
|
||||
--light-settings-border: #dee2e6;
|
||||
--light-settings-input-bg: #ffffff;
|
||||
--light-settings-input-border: #ced4da;
|
||||
--light-settings-hover: #e9ecef;
|
||||
--light-scrollbar-track: #f1f3f4;
|
||||
--light-scrollbar-thumb: #c1c1c1;
|
||||
--light-scrollbar-thumb-hover: #a8a8a8;
|
||||
--light-selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--light-selection-text: #2563eb;
|
||||
--light-danger-color: #dc3545;
|
||||
--light-bg-primary: #ffffff;
|
||||
--light-bg-hover: #f1f3f4;
|
||||
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--light-loading-color: #1a3c1a;
|
||||
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--light-loading-done-color: #008800;
|
||||
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* 默认使用深色主题 */
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--tab-active-line: var(--dark-tab-active-line);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
|
||||
|
||||
color-scheme: light dark;
|
||||
--voidraft-font-mono: "HarmonyOS", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
/* 监听系统深色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme="auto"] {
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--tab-active-line: var(--dark-tab-active-line);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
}
|
||||
/* 默认/暗色主题 */
|
||||
:root,
|
||||
:root[data-theme="dark"],
|
||||
:root[data-theme="auto"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--text-primary: #ffffff;
|
||||
|
||||
--toolbar-bg: #2d2d2d;
|
||||
--toolbar-border: #404040;
|
||||
--toolbar-text: #ffffff;
|
||||
--toolbar-text-secondary: #cccccc;
|
||||
--toolbar-button-hover: #404040;
|
||||
--toolbar-separator: #404040;
|
||||
|
||||
--tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
|
||||
--bg-secondary: #0e1217;
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-hover: #2a2a2a;
|
||||
|
||||
--text-secondary: #a0aec0;
|
||||
--text-muted: #666666;
|
||||
--text-danger: #ff6b6b;
|
||||
|
||||
--border-color: #2d3748;
|
||||
|
||||
--settings-bg: #2a2a2a;
|
||||
--settings-card-bg: #333333;
|
||||
--settings-text: #ffffff;
|
||||
--settings-text-secondary: #cccccc;
|
||||
--settings-border: #444444;
|
||||
--settings-input-bg: #3a3a3a;
|
||||
--settings-input-border: #555555;
|
||||
--settings-hover: #404040;
|
||||
|
||||
--scrollbar-track: #2a2a2a;
|
||||
--scrollbar-thumb: #555555;
|
||||
--scrollbar-thumb-hover: #666666;
|
||||
|
||||
--selection-bg: rgba(181, 206, 168, 0.1);
|
||||
--selection-text: #b5cea8;
|
||||
|
||||
--voidraft-bg-gradient: radial-gradient(#222922, #000500);
|
||||
--voidraft-loading-color: #ffffff;
|
||||
--voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
|
||||
--voidraft-loading-done-color: #66ff66;
|
||||
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
|
||||
|
||||
/* Markdown 代码块样式 - 暗色主题 */
|
||||
--cm-codeblock-bg: rgba(46, 51, 69, 0.8);
|
||||
--cm-codeblock-radius: 0.4rem;
|
||||
|
||||
|
||||
/* Markdown 内联代码样式 */
|
||||
--cm-inline-code-bg: oklch(28% 0.02 255);
|
||||
|
||||
/* Markdown 上标/下标样式 */
|
||||
--cm-superscript-color: inherit;
|
||||
--cm-subscript-color: inherit;
|
||||
|
||||
/* Markdown 高亮样式 */
|
||||
--cm-highlight-background: rgba(250, 204, 21, 0.35);
|
||||
|
||||
/* Markdown 表格样式 - 暗色主题 */
|
||||
--cm-table-bg: rgba(35, 40, 52, 0.5);
|
||||
--cm-table-header-bg: rgba(46, 51, 69, 0.7);
|
||||
--cm-table-border: rgba(75, 85, 99, 0.35);
|
||||
--cm-table-row-hover: rgba(55, 62, 78, 0.5);
|
||||
|
||||
/* Search Panel - Dark Theme */
|
||||
--search-panel-bg: #252526;
|
||||
--search-panel-text: #cccccc;
|
||||
--search-panel-border: #454545;
|
||||
--search-input-bg: #3c3c3c;
|
||||
--search-input-text: #cccccc;
|
||||
--search-input-border: #3c3c3c;
|
||||
--search-focus-border: #0078d4;
|
||||
--search-btn-hover: rgba(255, 255, 255, 0.1);
|
||||
--search-btn-active-bg: rgba(0, 120, 212, 0.4);
|
||||
--search-btn-active-text: #ffffff;
|
||||
--search-error-border: #f14c4c;
|
||||
--search-error-bg: #5a1d1d;
|
||||
|
||||
/* Search Match Highlight - Dark Theme (VSCode style) */
|
||||
--search-match-bg: rgba(250, 220, 81, 0.85);
|
||||
--search-match-selected-bg: rgba(81, 175, 255, 0.5);
|
||||
--search-match-selected-border: #74b0f4;
|
||||
}
|
||||
|
||||
/* 监听系统浅色主题 */
|
||||
/* 亮色主题 */
|
||||
:root[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
|
||||
--text-primary: #000000;
|
||||
|
||||
--toolbar-bg: #f8f9fa;
|
||||
--toolbar-border: #e9ecef;
|
||||
--toolbar-text: #212529;
|
||||
--toolbar-text-secondary: #495057;
|
||||
--toolbar-button-hover: #e9ecef;
|
||||
--toolbar-separator: #e9ecef;
|
||||
|
||||
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||
--bg-secondary: #f7fef7;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-hover: #f1f3f4;
|
||||
|
||||
--text-secondary: #374151;
|
||||
--text-muted: #6b7280;
|
||||
--text-danger: #dc3545;
|
||||
|
||||
--border-color: #e5e7eb;
|
||||
|
||||
--settings-bg: #ffffff;
|
||||
--settings-card-bg: #f8f9fa;
|
||||
--settings-text: #212529;
|
||||
--settings-text-secondary: #6c757d;
|
||||
--settings-border: #dee2e6;
|
||||
--settings-input-bg: #ffffff;
|
||||
--settings-input-border: #ced4da;
|
||||
--settings-hover: #e9ecef;
|
||||
|
||||
--scrollbar-track: #f1f3f4;
|
||||
--scrollbar-thumb: #c1c1c1;
|
||||
--scrollbar-thumb-hover: #a8a8a8;
|
||||
|
||||
--selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--selection-text: #2563eb;
|
||||
|
||||
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--voidraft-loading-color: #1a3c1a;
|
||||
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--voidraft-loading-done-color: #008800;
|
||||
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* Markdown 代码块样式 - 亮色主题 */
|
||||
--cm-codeblock-bg: #f3f3f3;
|
||||
--cm-codeblock-radius: 0.4rem;
|
||||
|
||||
/* Markdown 内联代码样式 */
|
||||
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
|
||||
|
||||
/* Markdown 上标/下标样式 */
|
||||
--cm-superscript-color: inherit;
|
||||
--cm-subscript-color: inherit;
|
||||
|
||||
/* Markdown 高亮样式 */
|
||||
--cm-highlight-background: rgba(253, 224, 71, 0.45);
|
||||
|
||||
/* Markdown 表格样式 - 亮色主题 */
|
||||
--cm-table-bg: oklch(97.5% 0.006 255);
|
||||
--cm-table-header-bg: oklch(94% 0.01 255);
|
||||
--cm-table-border: oklch(88% 0.008 255);
|
||||
--cm-table-row-hover: oklch(95% 0.008 255);
|
||||
|
||||
/* Search Panel - Light Theme */
|
||||
--search-panel-bg: #f3f3f3;
|
||||
--search-panel-text: #616161;
|
||||
--search-panel-border: #c8c8c8;
|
||||
--search-input-bg: #ffffff;
|
||||
--search-input-text: #616161;
|
||||
--search-input-border: #cecece;
|
||||
--search-focus-border: #0078d4;
|
||||
--search-btn-hover: rgba(0, 0, 0, 0.1);
|
||||
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
|
||||
--search-btn-active-text: #0078d4;
|
||||
--search-error-border: #e51400;
|
||||
--search-error-bg: #fdeceb;
|
||||
|
||||
/* Search Match Highlight - Light Theme (VSCode style) */
|
||||
--search-match-bg: rgba(250, 220, 81, 0.85);
|
||||
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
|
||||
--search-match-selected-border: #268fff;
|
||||
}
|
||||
|
||||
/* 跟随系统的浅色偏好 */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root[data-theme="auto"] {
|
||||
--toolbar-bg: var(--light-toolbar-bg);
|
||||
--toolbar-border: var(--light-toolbar-border);
|
||||
--toolbar-text: var(--light-toolbar-text);
|
||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||
--tab-active-line: var(--light-tab-active-line);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--text-muted: var(--light-text-muted);
|
||||
--border-color: var(--light-border-color);
|
||||
--settings-bg: var(--light-settings-bg);
|
||||
--settings-card-bg: var(--light-settings-card-bg);
|
||||
--settings-text: var(--light-settings-text);
|
||||
--settings-text-secondary: var(--light-settings-text-secondary);
|
||||
--settings-border: var(--light-settings-border);
|
||||
--settings-input-bg: var(--light-settings-input-bg);
|
||||
--settings-input-border: var(--light-settings-input-border);
|
||||
--settings-hover: var(--light-settings-hover);
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--light-loading-color);
|
||||
--voidraft-loading-glow: var(--light-loading-glow);
|
||||
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||
color-scheme: light;
|
||||
|
||||
--text-primary: #000000;
|
||||
|
||||
--toolbar-bg: #f8f9fa;
|
||||
--toolbar-border: #e9ecef;
|
||||
--toolbar-text: #212529;
|
||||
--toolbar-text-secondary: #495057;
|
||||
--toolbar-button-hover: #e9ecef;
|
||||
--toolbar-separator: #e9ecef;
|
||||
|
||||
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
|
||||
--bg-secondary: #f7fef7;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-hover: #f1f3f4;
|
||||
|
||||
--text-secondary: #374151;
|
||||
--text-muted: #6b7280;
|
||||
--text-danger: #dc3545;
|
||||
|
||||
--border-color: #e5e7eb;
|
||||
|
||||
--settings-bg: #ffffff;
|
||||
--settings-card-bg: #f8f9fa;
|
||||
--settings-text: #212529;
|
||||
--settings-text-secondary: #6c757d;
|
||||
--settings-border: #dee2e6;
|
||||
--settings-input-bg: #ffffff;
|
||||
--settings-input-border: #ced4da;
|
||||
--settings-hover: #e9ecef;
|
||||
|
||||
--scrollbar-track: #f1f3f4;
|
||||
--scrollbar-thumb: #c1c1c1;
|
||||
--scrollbar-thumb-hover: #a8a8a8;
|
||||
|
||||
--selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--selection-text: #2563eb;
|
||||
|
||||
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--voidraft-loading-color: #1a3c1a;
|
||||
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--voidraft-loading-done-color: #008800;
|
||||
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* Markdown 代码块样式 - 亮色主题 */
|
||||
--cm-codeblock-bg: oklch(92.9% 0.013 255.508);
|
||||
--cm-codeblock-radius: 0.4rem;
|
||||
|
||||
/* Markdown 内联代码样式 */
|
||||
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
|
||||
|
||||
/* Markdown 上标/下标样式 */
|
||||
--cm-superscript-color: inherit;
|
||||
--cm-subscript-color: inherit;
|
||||
|
||||
/* Markdown 高亮样式 */
|
||||
--cm-highlight-background: rgba(253, 224, 71, 0.45);
|
||||
|
||||
/* Markdown 表格样式 - 亮色主题 */
|
||||
--cm-table-bg: oklch(97.5% 0.006 255);
|
||||
--cm-table-header-bg: oklch(94% 0.01 255);
|
||||
--cm-table-border: oklch(88% 0.008 255);
|
||||
--cm-table-row-hover: oklch(95% 0.008 255);
|
||||
|
||||
/* Search Panel - Light Theme (auto) */
|
||||
--search-panel-bg: #f3f3f3;
|
||||
--search-panel-text: #616161;
|
||||
--search-panel-border: #c8c8c8;
|
||||
--search-input-bg: #ffffff;
|
||||
--search-input-text: #616161;
|
||||
--search-input-border: #cecece;
|
||||
--search-focus-border: #0078d4;
|
||||
--search-btn-hover: rgba(0, 0, 0, 0.1);
|
||||
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
|
||||
--search-btn-active-text: #0078d4;
|
||||
--search-error-border: #e51400;
|
||||
--search-error-bg: #fdeceb;
|
||||
|
||||
/* Search Match Highlight - Light Theme auto (VSCode style) */
|
||||
--search-match-bg: rgba(250, 220, 81, 0.85);
|
||||
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
|
||||
--search-match-selected-border: #268fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手动选择浅色主题 */
|
||||
:root[data-theme="light"] {
|
||||
--toolbar-bg: var(--light-toolbar-bg);
|
||||
--toolbar-border: var(--light-toolbar-border);
|
||||
--toolbar-text: var(--light-toolbar-text);
|
||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||
--tab-active-line: var(--light-tab-active-line);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--text-muted: var(--light-text-muted);
|
||||
--border-color: var(--light-border-color);
|
||||
--settings-bg: var(--light-settings-bg);
|
||||
--settings-card-bg: var(--light-settings-card-bg);
|
||||
--settings-text: var(--light-settings-text);
|
||||
--settings-text-secondary: var(--light-settings-text-secondary);
|
||||
--settings-border: var(--light-settings-border);
|
||||
--settings-input-bg: var(--light-settings-input-bg);
|
||||
--settings-input-border: var(--light-settings-input-border);
|
||||
--settings-hover: var(--light-settings-hover);
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--light-loading-color);
|
||||
--voidraft-loading-glow: var(--light-loading-glow);
|
||||
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||
}
|
||||
|
||||
/* 手动选择深色主题 */
|
||||
:root[data-theme="dark"] {
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--tab-active-line: var(--dark-tab-active-line);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
}
|
||||
@@ -1,43 +1,19 @@
|
||||
import {
|
||||
AppConfig,
|
||||
AppearanceConfig,
|
||||
EditingConfig,
|
||||
GeneralConfig,
|
||||
AuthMethod,
|
||||
LanguageType,
|
||||
SystemThemeType,
|
||||
TabType,
|
||||
UpdatesConfig,
|
||||
UpdateSourceType,
|
||||
GitBackupConfig,
|
||||
AuthMethod
|
||||
UpdateSourceType
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {FONT_OPTIONS} from './fonts';
|
||||
|
||||
// 配置键映射和限制的类型定义
|
||||
export type GeneralConfigKeyMap = {
|
||||
readonly [K in keyof GeneralConfig]: string;
|
||||
};
|
||||
|
||||
export type EditingConfigKeyMap = {
|
||||
readonly [K in keyof EditingConfig]: string;
|
||||
};
|
||||
|
||||
export type AppearanceConfigKeyMap = {
|
||||
readonly [K in keyof AppearanceConfig]: string;
|
||||
};
|
||||
|
||||
export type UpdatesConfigKeyMap = {
|
||||
readonly [K in keyof UpdatesConfig]: string;
|
||||
};
|
||||
|
||||
export type BackupConfigKeyMap = {
|
||||
readonly [K in keyof GitBackupConfig]: string;
|
||||
};
|
||||
|
||||
export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
||||
export type ConfigSection = 'general' | 'editing' | 'appearance' | 'updates' | 'backup';
|
||||
|
||||
// 配置键映射
|
||||
export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
// 统一配置键映射(平级展开)
|
||||
export const CONFIG_KEY_MAP = {
|
||||
// general
|
||||
alwaysOnTop: 'general.alwaysOnTop',
|
||||
dataPath: 'general.dataPath',
|
||||
enableSystemTray: 'general.enableSystemTray',
|
||||
@@ -47,9 +23,7 @@ export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
enableWindowSnap: 'general.enableWindowSnap',
|
||||
enableLoadingAnimation: 'general.enableLoadingAnimation',
|
||||
enableTabs: 'general.enableTabs',
|
||||
} as const;
|
||||
|
||||
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
// editing
|
||||
fontSize: 'editing.fontSize',
|
||||
fontFamily: 'editing.fontFamily',
|
||||
fontWeight: 'editing.fontWeight',
|
||||
@@ -57,16 +31,12 @@ export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
enableTabIndent: 'editing.enableTabIndent',
|
||||
tabSize: 'editing.tabSize',
|
||||
tabType: 'editing.tabType',
|
||||
autoSaveDelay: 'editing.autoSaveDelay'
|
||||
} as const;
|
||||
|
||||
export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||
autoSaveDelay: 'editing.autoSaveDelay',
|
||||
// appearance
|
||||
language: 'appearance.language',
|
||||
systemTheme: 'appearance.systemTheme',
|
||||
currentTheme: 'appearance.currentTheme'
|
||||
} as const;
|
||||
|
||||
export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
currentTheme: 'appearance.currentTheme',
|
||||
// updates
|
||||
version: 'updates.version',
|
||||
autoUpdate: 'updates.autoUpdate',
|
||||
primarySource: 'updates.primarySource',
|
||||
@@ -74,10 +44,8 @@ export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
backupBeforeUpdate: 'updates.backupBeforeUpdate',
|
||||
updateTimeout: 'updates.updateTimeout',
|
||||
github: 'updates.github',
|
||||
gitea: 'updates.gitea'
|
||||
} as const;
|
||||
|
||||
export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||
gitea: 'updates.gitea',
|
||||
// backup
|
||||
enabled: 'backup.enabled',
|
||||
repo_url: 'backup.repo_url',
|
||||
auth_method: 'backup.auth_method',
|
||||
@@ -90,6 +58,8 @@ export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||
auto_backup: 'backup.auto_backup',
|
||||
} as const;
|
||||
|
||||
export type ConfigKey = keyof typeof CONFIG_KEY_MAP;
|
||||
|
||||
// 配置限制
|
||||
export const CONFIG_LIMITS = {
|
||||
fontSize: {min: 12, max: 28, default: 13},
|
||||
|
||||
1945
frontend/src/common/constant/emojies.ts
Normal file
1945
frontend/src/common/constant/emojies.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,10 @@ export const FONT_OPTIONS = [
|
||||
label: 'Open Sans',
|
||||
value: '"Open Sans"'
|
||||
},
|
||||
{
|
||||
label: 'Monocraft',
|
||||
value: 'Monocraft'
|
||||
},
|
||||
// Common system fonts
|
||||
{
|
||||
label: 'Arial',
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* 默认翻译配置
|
||||
*/
|
||||
export const DEFAULT_TRANSLATION_CONFIG = {
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译相关的错误消息
|
||||
*/
|
||||
export const TRANSLATION_ERRORS = {
|
||||
NO_TEXT: 'no text to translate',
|
||||
TRANSLATION_FAILED: 'translation failed',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译结果接口
|
||||
*/
|
||||
export interface TranslationResult {
|
||||
translatedText: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言信息接口
|
||||
*/
|
||||
export interface LanguageInfo {
|
||||
Code: string; // 语言代码
|
||||
Name: string; // 语言名称
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译器扩展配置
|
||||
*/
|
||||
export interface TranslatorConfig {
|
||||
/** 最小选择字符数才显示翻译按钮 */
|
||||
minSelectionLength: number;
|
||||
/** 最大翻译字符数 */
|
||||
maxTranslationLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
export const TRANSLATION_ICON_SVG = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
329
frontend/src/common/utils/domDiff.test.ts
Normal file
329
frontend/src/common/utils/domDiff.test.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* DOM Diff 算法单元测试
|
||||
*/
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { morphNode, morphHTML, morphWithKeys } from './domDiff';
|
||||
|
||||
describe('DOM Diff Algorithm', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
describe('morphNode - 基础功能', () => {
|
||||
test('应该更新文本节点内容', () => {
|
||||
const fromNode = document.createTextNode('Hello');
|
||||
const toNode = document.createTextNode('World');
|
||||
container.appendChild(fromNode);
|
||||
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(fromNode.nodeValue).toBe('World');
|
||||
});
|
||||
|
||||
test('应该保持相同的文本节点不变', () => {
|
||||
const fromNode = document.createTextNode('Hello');
|
||||
const toNode = document.createTextNode('Hello');
|
||||
container.appendChild(fromNode);
|
||||
|
||||
const originalNode = fromNode;
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(fromNode).toBe(originalNode);
|
||||
expect(fromNode.nodeValue).toBe('Hello');
|
||||
});
|
||||
|
||||
test('应该替换不同类型的节点', () => {
|
||||
const fromNode = document.createElement('span');
|
||||
fromNode.textContent = 'Hello';
|
||||
const toNode = document.createElement('div');
|
||||
toNode.textContent = 'World';
|
||||
container.appendChild(fromNode);
|
||||
|
||||
morphNode(fromNode, toNode);
|
||||
|
||||
expect(container.firstChild?.nodeName).toBe('DIV');
|
||||
expect(container.firstChild?.textContent).toBe('World');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphNode - 属性更新', () => {
|
||||
test('应该添加新属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'test');
|
||||
toEl.setAttribute('id', 'myid');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('test');
|
||||
expect(fromEl.getAttribute('id')).toBe('myid');
|
||||
});
|
||||
|
||||
test('应该更新已存在的属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.setAttribute('class', 'old');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'new');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('new');
|
||||
});
|
||||
|
||||
test('应该删除不存在的属性', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.setAttribute('class', 'test');
|
||||
fromEl.setAttribute('id', 'myid');
|
||||
const toEl = document.createElement('div');
|
||||
toEl.setAttribute('class', 'test');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.getAttribute('class')).toBe('test');
|
||||
expect(fromEl.hasAttribute('id')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphNode - 子节点更新', () => {
|
||||
test('应该添加新子节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = '<li>1</li><li>2</li>';
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(3);
|
||||
expect(fromEl.children[2].textContent).toBe('3');
|
||||
});
|
||||
|
||||
test('应该删除多余的子节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = '<li>1</li><li>2</li>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(2);
|
||||
expect(fromEl.textContent).toBe('12');
|
||||
});
|
||||
|
||||
test('应该更新子节点内容', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.innerHTML = '<p>Old</p>';
|
||||
const toEl = document.createElement('div');
|
||||
toEl.innerHTML = '<p>New</p>';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const originalP = fromEl.querySelector('p');
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
// 应该保持同一个 p 元素,只更新内容
|
||||
expect(fromEl.querySelector('p')).toBe(originalP);
|
||||
expect(fromEl.querySelector('p')?.textContent).toBe('New');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphHTML - HTML 字符串更新', () => {
|
||||
test('应该从 HTML 字符串更新元素', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = '<p>Old</p>';
|
||||
container.appendChild(element);
|
||||
|
||||
morphHTML(element, '<p>New</p>');
|
||||
|
||||
expect(element.innerHTML).toBe('<p>New</p>');
|
||||
});
|
||||
|
||||
test('应该处理复杂的 HTML 结构', () => {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = '<h1>Title</h1><p>Paragraph</p>';
|
||||
container.appendChild(element);
|
||||
|
||||
morphHTML(element, '<h1>New Title</h1><p>New Paragraph</p><span>Extra</span>');
|
||||
|
||||
expect(element.children.length).toBe(3);
|
||||
expect(element.querySelector('h1')?.textContent).toBe('New Title');
|
||||
expect(element.querySelector('p')?.textContent).toBe('New Paragraph');
|
||||
expect(element.querySelector('span')?.textContent).toBe('Extra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('morphWithKeys - 基于 key 的智能 diff', () => {
|
||||
test('应该保持相同 key 的节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A Updated</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const originalA = fromEl.querySelector('[data-key="a"]');
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA);
|
||||
expect(originalA?.textContent).toBe('A Updated');
|
||||
});
|
||||
|
||||
test('应该重新排序节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="c">C</li>
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key'));
|
||||
expect(keys).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
test('应该添加新的 key 节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(3);
|
||||
expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C');
|
||||
});
|
||||
|
||||
test('应该删除不存在的 key 节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
fromEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="b">B</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
const toEl = document.createElement('ul');
|
||||
toEl.innerHTML = `
|
||||
<li data-key="a">A</li>
|
||||
<li data-key="c">C</li>
|
||||
`;
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphWithKeys(fromEl, toEl);
|
||||
|
||||
expect(fromEl.children.length).toBe(2);
|
||||
expect(fromEl.querySelector('[data-key="b"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('性能测试', () => {
|
||||
test('应该高效处理大量节点', () => {
|
||||
const fromEl = document.createElement('ul');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Item ${i}`;
|
||||
fromEl.appendChild(li);
|
||||
}
|
||||
|
||||
const toEl = document.createElement('ul');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Updated Item ${i}`;
|
||||
toEl.appendChild(li);
|
||||
}
|
||||
|
||||
container.appendChild(fromEl);
|
||||
|
||||
const startTime = performance.now();
|
||||
morphNode(fromEl, toEl);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
|
||||
expect(fromEl.children.length).toBe(1000);
|
||||
expect(fromEl.children[0].textContent).toBe('Updated Item 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
test('应该处理空节点', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
const toEl = document.createElement('div');
|
||||
container.appendChild(fromEl);
|
||||
|
||||
expect(() => morphNode(fromEl, toEl)).not.toThrow();
|
||||
});
|
||||
|
||||
test('应该处理只有文本的节点', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.textContent = 'Hello';
|
||||
const toEl = document.createElement('div');
|
||||
toEl.textContent = 'World';
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.textContent).toBe('World');
|
||||
});
|
||||
|
||||
test('应该处理嵌套的复杂结构', () => {
|
||||
const fromEl = document.createElement('div');
|
||||
fromEl.innerHTML = `
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<span>Text</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const toEl = document.createElement('div');
|
||||
toEl.innerHTML = `
|
||||
<div class="outer modified">
|
||||
<div class="inner">
|
||||
<span>Updated Text</span>
|
||||
<strong>New</strong>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(fromEl);
|
||||
|
||||
morphNode(fromEl, toEl);
|
||||
|
||||
expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true);
|
||||
expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text');
|
||||
expect(fromEl.querySelector('strong')?.textContent).toBe('New');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
180
frontend/src/common/utils/domDiff.ts
Normal file
180
frontend/src/common/utils/domDiff.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 轻量级 DOM Diff 算法实现
|
||||
* 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动
|
||||
*/
|
||||
|
||||
/**
|
||||
* 比较并更新两个 DOM 节点
|
||||
* @param fromNode 原节点
|
||||
* @param toNode 目标节点
|
||||
*/
|
||||
export function morphNode(fromNode: Node, toNode: Node): void {
|
||||
// 节点类型不同,直接替换
|
||||
if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) {
|
||||
fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 文本节点:比较内容
|
||||
if (fromNode.nodeType === Node.TEXT_NODE) {
|
||||
if (fromNode.nodeValue !== toNode.nodeValue) {
|
||||
fromNode.nodeValue = toNode.nodeValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 元素节点:更新属性和子节点
|
||||
if (fromNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const fromEl = fromNode as Element;
|
||||
const toEl = toNode as Element;
|
||||
|
||||
// 更新属性
|
||||
morphAttributes(fromEl, toEl);
|
||||
|
||||
// 更新子节点
|
||||
morphChildren(fromEl, toEl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元素属性
|
||||
*/
|
||||
function morphAttributes(fromEl: Element, toEl: Element): void {
|
||||
// 移除旧属性
|
||||
const fromAttrs = fromEl.attributes;
|
||||
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
||||
const attr = fromAttrs[i];
|
||||
if (!toEl.hasAttribute(attr.name)) {
|
||||
fromEl.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加/更新新属性
|
||||
const toAttrs = toEl.attributes;
|
||||
for (let i = 0; i < toAttrs.length; i++) {
|
||||
const attr = toAttrs[i];
|
||||
const fromValue = fromEl.getAttribute(attr.name);
|
||||
if (fromValue !== attr.value) {
|
||||
fromEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新子节点(核心 diff 算法)
|
||||
*/
|
||||
function morphChildren(fromEl: Element, toEl: Element): void {
|
||||
const fromChildren = Array.from(fromEl.childNodes);
|
||||
const toChildren = Array.from(toEl.childNodes);
|
||||
|
||||
const fromLen = fromChildren.length;
|
||||
const toLen = toChildren.length;
|
||||
const minLen = Math.min(fromLen, toLen);
|
||||
|
||||
// 1. 更新公共部分
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
morphNode(fromChildren[i], toChildren[i]);
|
||||
}
|
||||
|
||||
// 2. 移除多余的旧节点
|
||||
if (fromLen > toLen) {
|
||||
for (let i = fromLen - 1; i >= toLen; i--) {
|
||||
fromEl.removeChild(fromChildren[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 添加新节点
|
||||
if (toLen > fromLen) {
|
||||
for (let i = fromLen; i < toLen; i++) {
|
||||
fromEl.appendChild(toChildren[i].cloneNode(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化版:使用 key 进行更智能的 diff(可选)
|
||||
* 适用于有 data-key 属性的元素
|
||||
*/
|
||||
export function morphWithKeys(fromEl: Element, toEl: Element): void {
|
||||
const toChildren = Array.from(toEl.children) as Element[];
|
||||
|
||||
// 构建 from 的 key 映射
|
||||
const fromKeyMap = new Map<string, Element>();
|
||||
Array.from(fromEl.children).forEach((child) => {
|
||||
const key = child.getAttribute('data-key');
|
||||
if (key) {
|
||||
fromKeyMap.set(key, child);
|
||||
}
|
||||
});
|
||||
|
||||
const processedKeys = new Set<string>();
|
||||
|
||||
// 按照 toChildren 的顺序处理
|
||||
toChildren.forEach((toChild, toIndex) => {
|
||||
const key = toChild.getAttribute('data-key');
|
||||
if (!key) return;
|
||||
|
||||
processedKeys.add(key);
|
||||
const fromChild = fromKeyMap.get(key);
|
||||
|
||||
if (fromChild) {
|
||||
// 找到对应节点,更新内容
|
||||
morphNode(fromChild, toChild);
|
||||
|
||||
// 确保节点在正确的位置
|
||||
const currentNode = fromEl.children[toIndex];
|
||||
if (currentNode !== fromChild) {
|
||||
// 将 fromChild 移动到正确位置
|
||||
fromEl.insertBefore(fromChild, currentNode);
|
||||
}
|
||||
} else {
|
||||
// 新节点,插入到正确位置
|
||||
const currentNode = fromEl.children[toIndex];
|
||||
fromEl.insertBefore(toChild.cloneNode(true), currentNode || null);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除不再存在的节点(从后往前删除,避免索引问题)
|
||||
const childrenToRemove: Element[] = [];
|
||||
fromKeyMap.forEach((child, key) => {
|
||||
if (!processedKeys.has(key)) {
|
||||
childrenToRemove.push(child);
|
||||
}
|
||||
});
|
||||
childrenToRemove.forEach(child => {
|
||||
fromEl.removeChild(child);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级 API:直接从 HTML 字符串更新元素
|
||||
*/
|
||||
export function morphHTML(element: Element, htmlString: string): void {
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = htmlString;
|
||||
|
||||
// 更新元素的子节点列表
|
||||
morphChildren(element, tempContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新(使用 DocumentFragment)
|
||||
*/
|
||||
export function batchMorph(element: Element, htmlString: string): void {
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = htmlString;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
Array.from(tempContainer.childNodes).forEach(node => {
|
||||
fragment.appendChild(node);
|
||||
});
|
||||
|
||||
// 清空原内容
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
|
||||
// 批量插入
|
||||
element.appendChild(fragment);
|
||||
}
|
||||
|
||||
65
frontend/src/common/utils/formatter.ts
Normal file
65
frontend/src/common/utils/formatter.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Formatter utility functions
|
||||
*/
|
||||
|
||||
export interface DateTimeFormatOptions {
|
||||
locale?: string;
|
||||
includeTime?: boolean;
|
||||
hour12?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date time string to localized format
|
||||
* @param dateString - ISO date string or null
|
||||
* @param options - Formatting options
|
||||
* @returns Formatted date string or error message
|
||||
*/
|
||||
export const formatDateTime = (
|
||||
dateString: string | null,
|
||||
options: DateTimeFormatOptions = {}
|
||||
): string => {
|
||||
const {
|
||||
locale = 'en-US',
|
||||
includeTime = true,
|
||||
hour12 = false
|
||||
} = options;
|
||||
|
||||
if (!dateString) {
|
||||
return 'Unknown time';
|
||||
}
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Invalid date';
|
||||
}
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
};
|
||||
|
||||
if (includeTime) {
|
||||
formatOptions.hour = '2-digit';
|
||||
formatOptions.minute = '2-digit';
|
||||
formatOptions.hour12 = hour12;
|
||||
}
|
||||
|
||||
return date.toLocaleString(locale, formatOptions);
|
||||
} catch {
|
||||
return 'Time error';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncate string with ellipsis
|
||||
* @param str - String to truncate
|
||||
* @param maxLength - Maximum length before truncation
|
||||
* @returns Truncated string with ellipsis if needed
|
||||
*/
|
||||
export const truncateString = (str: string, maxLength: number): string => {
|
||||
if (!str) return '';
|
||||
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
||||
};
|
||||
42
frontend/src/common/utils/validation.ts
Normal file
42
frontend/src/common/utils/validation.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Validation utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate document title
|
||||
* @param title - The title to validate
|
||||
* @param maxLength - Maximum allowed length (default: 50)
|
||||
* @returns Error message if invalid, null if valid
|
||||
*/
|
||||
export const validateDocumentTitle = (title: string, maxLength: number = 50): string | null => {
|
||||
const trimmed = title.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return 'Document name cannot be empty';
|
||||
}
|
||||
|
||||
if (trimmed.length > maxLength) {
|
||||
return `Document name cannot exceed ${maxLength} characters`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a string is empty or whitespace only
|
||||
* @param value - The string to check
|
||||
* @returns true if empty or whitespace only
|
||||
*/
|
||||
export const isEmpty = (value: string | null | undefined): boolean => {
|
||||
return !value || value.trim().length === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a string exceeds max length
|
||||
* @param value - The string to check
|
||||
* @param maxLength - Maximum allowed length
|
||||
* @returns true if exceeds max length
|
||||
*/
|
||||
export const exceedsMaxLength = (value: string, maxLength: number): boolean => {
|
||||
return value.trim().length > maxLength;
|
||||
};
|
||||
@@ -142,7 +142,7 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--voidraft-mono-font, monospace),serif;
|
||||
font-family: Menlo, monospace,serif;
|
||||
}
|
||||
|
||||
.loading-word {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && canClose"
|
||||
v-click-outside="handleClose"
|
||||
class="tab-context-menu"
|
||||
:style="{
|
||||
left: position.x + 'px',
|
||||
@@ -9,27 +10,31 @@
|
||||
@click.stop
|
||||
>
|
||||
<div v-if="canClose" class="menu-item" @click="handleMenuClick('close')">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
<span class="menu-text">{{ t('tabs.contextMenu.closeTab') }}</span>
|
||||
</div>
|
||||
<div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<path d="M9 9l6 6M15 9l-6 6"/>
|
||||
</svg>
|
||||
<span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span>
|
||||
</div>
|
||||
<div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
<path d="M9 18l-6-6 6-6"/>
|
||||
</svg>
|
||||
<span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span>
|
||||
</div>
|
||||
<div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18l6-6-6-6"/>
|
||||
<path d="M15 18l6-6-6-6"/>
|
||||
</svg>
|
||||
@@ -39,9 +44,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useTabStore } from '@/stores/tabStore';
|
||||
import {computed, onMounted, onUnmounted} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
@@ -54,7 +59,7 @@ const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const {t} = useI18n();
|
||||
const tabStore = useTabStore();
|
||||
|
||||
// 计算属性
|
||||
@@ -79,6 +84,9 @@ const hasTabsToLeft = computed(() => {
|
||||
return index > 0;
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
// 处理菜单项点击
|
||||
const handleMenuClick = (action: string) => {
|
||||
if (!props.targetDocumentId) return;
|
||||
@@ -98,33 +106,8 @@ const handleMenuClick = (action: string) => {
|
||||
break;
|
||||
}
|
||||
|
||||
emit('close');
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// 处理外部点击
|
||||
const handleClickOutside = (_event: MouseEvent) => {
|
||||
if (props.visible) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理ESC键
|
||||
const handleEscapeKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && props.visible) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -147,7 +130,7 @@ onUnmounted(() => {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.15s ease;
|
||||
gap: 8px;
|
||||
|
||||
@@ -165,7 +148,7 @@ onUnmounted(() => {
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-primary);
|
||||
transition: color 0.15s ease;
|
||||
|
||||
.menu-item:hover & {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-icon">
|
||||
<img src="/appicon.png" alt="voidraft"/>
|
||||
</div>
|
||||
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
|
||||
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
|
||||
{{ titleText }}
|
||||
</div>
|
||||
<!-- 标签页容器区域 -->
|
||||
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
|
||||
<TabContainer />
|
||||
<TabContainer/>
|
||||
</div>
|
||||
<!-- 设置页面标题 -->
|
||||
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
|
||||
@@ -26,7 +28,7 @@
|
||||
|
||||
<button
|
||||
class="titlebar-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
@click="handleToggleMaximize"
|
||||
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
|
||||
@@ -55,81 +57,43 @@
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useRoute} from 'vue-router';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
import TabContainer from '@/components/tabs/TabContainer.vue';
|
||||
import {useTabStore} from "@/stores/tabStore";
|
||||
import {
|
||||
minimizeWindow,
|
||||
toggleMaximize,
|
||||
closeWindow,
|
||||
getMaximizedState,
|
||||
generateTitleText,
|
||||
generateFullTitleText
|
||||
} from './index';
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const {t} = useI18n();
|
||||
const route = useRoute();
|
||||
const isMaximized = ref(false);
|
||||
const tabStore = useTabStore();
|
||||
const documentStore = useDocumentStore();
|
||||
|
||||
// 判断是否在设置页面
|
||||
const isMaximized = ref(false);
|
||||
const isInSettings = computed(() => route.path.startsWith('/settings'));
|
||||
|
||||
// 计算标题文本
|
||||
const titleText = computed(() => {
|
||||
if (isInSettings.value) {
|
||||
return `voidraft - ` + t('settings.title');
|
||||
}
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
if (currentDoc) {
|
||||
// 限制文档标题长度,避免标题栏换行
|
||||
const maxTitleLength = 30;
|
||||
const truncatedTitle = currentDoc.title.length > maxTitleLength
|
||||
? currentDoc.title.substring(0, maxTitleLength) + '...'
|
||||
: currentDoc.title;
|
||||
return `voidraft - ${truncatedTitle}`;
|
||||
}
|
||||
return 'voidraft';
|
||||
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
|
||||
return generateTitleText(documentStore.currentDocument?.title);
|
||||
});
|
||||
|
||||
// 计算完整标题文本(用于tooltip)
|
||||
const fullTitleText = computed(() => {
|
||||
if (isInSettings.value) {
|
||||
return `voidraft - ` + t('settings.title');
|
||||
}
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
|
||||
return generateFullTitleText(documentStore.currentDocument?.title);
|
||||
});
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
await runtime.Window.ToggleMaximise();
|
||||
await checkMaximizedState();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
const handleToggleMaximize = async () => {
|
||||
await toggleMaximize();
|
||||
isMaximized.value = await getMaximizedState();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
isMaximized.value = await getMaximizedState();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -160,7 +124,7 @@ onMounted(async () => {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
min-width: 0; /* 允许内容收缩 */
|
||||
min-width: 0;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<button
|
||||
class="titlebar-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
@click="handleToggleMaximize"
|
||||
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
|
||||
>
|
||||
<div class="button-icon">
|
||||
@@ -45,95 +45,58 @@
|
||||
|
||||
<!-- 标签页容器区域 -->
|
||||
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
|
||||
<TabContainer />
|
||||
<TabContainer/>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent v-if="!tabStore.isTabsEnabled || isInSettings">
|
||||
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent
|
||||
v-if="!tabStore.isTabsEnabled || isInSettings">
|
||||
<div class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useRoute} from 'vue-router';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
import TabContainer from '@/components/tabs/TabContainer.vue';
|
||||
import { useTabStore } from "@/stores/tabStore";
|
||||
import {
|
||||
minimizeWindow,
|
||||
toggleMaximize,
|
||||
closeWindow,
|
||||
getMaximizedState,
|
||||
generateTitleText,
|
||||
generateFullTitleText
|
||||
} from './index';
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const { t } = useI18n();
|
||||
const {t} = useI18n();
|
||||
const route = useRoute();
|
||||
const isMaximized = ref(false);
|
||||
const showControlIcons = ref(false);
|
||||
const tabStore = useTabStore();
|
||||
const documentStore = useDocumentStore();
|
||||
|
||||
// 判断是否在设置页面
|
||||
const isMaximized = ref(false);
|
||||
const showControlIcons = ref(false);
|
||||
const isInSettings = computed(() => route.path.startsWith('/settings'));
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
await runtime.Window.ToggleMaximise();
|
||||
await checkMaximizedState();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算标题文本
|
||||
const titleText = computed(() => {
|
||||
if (isInSettings.value) {
|
||||
return `voidraft - ` + t('settings.title');
|
||||
}
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
if (currentDoc) {
|
||||
// 限制文档标题长度,避免标题栏换行
|
||||
const maxTitleLength = 30;
|
||||
const truncatedTitle = currentDoc.title.length > maxTitleLength
|
||||
? currentDoc.title.substring(0, maxTitleLength) + '...'
|
||||
: currentDoc.title;
|
||||
return `voidraft - ${truncatedTitle}`;
|
||||
}
|
||||
return 'voidraft';
|
||||
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
|
||||
return generateTitleText(documentStore.currentDocument?.title);
|
||||
});
|
||||
|
||||
// 计算完整标题文本(用于tooltip)
|
||||
const fullTitleText = computed(() => {
|
||||
if (isInSettings.value) {
|
||||
return `voidraft - ` + t('settings.title');
|
||||
}
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
|
||||
return generateFullTitleText(documentStore.currentDocument?.title);
|
||||
});
|
||||
|
||||
const handleToggleMaximize = async () => {
|
||||
await toggleMaximize();
|
||||
isMaximized.value = await getMaximizedState();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
isMaximized.value = await getMaximizedState();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -261,9 +224,8 @@ onMounted(async () => {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
min-width: 0;
|
||||
overflow: visible; /* 允许TabContainer内部处理滚动 */
|
||||
overflow: visible;
|
||||
|
||||
/* 确保TabContainer能够正确处理滚动 */
|
||||
:deep(.tab-container) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -285,7 +247,6 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保底部线条能够正确显示 */
|
||||
:deep(.tab-item) {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="windows-titlebar" style="--wails-draggable:drag">
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-icon">
|
||||
<img src="/appicon.png" alt="voidraft"/>
|
||||
</div>
|
||||
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
|
||||
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
|
||||
{{ titleText }}
|
||||
</div>
|
||||
<!-- 标签页容器区域 -->
|
||||
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
|
||||
<TabContainer />
|
||||
<TabContainer/>
|
||||
</div>
|
||||
<!-- 设置页面标题 -->
|
||||
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
|
||||
@@ -24,7 +26,7 @@
|
||||
|
||||
<button
|
||||
class="titlebar-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
@click="handleToggleMaximize"
|
||||
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
|
||||
>
|
||||
<span class="titlebar-icon" v-html="maximizeIcon"></span>
|
||||
@@ -45,84 +47,44 @@
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useRoute} from 'vue-router';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
import TabContainer from '@/components/tabs/TabContainer.vue';
|
||||
import {useTabStore} from "@/stores/tabStore";
|
||||
import {
|
||||
minimizeWindow,
|
||||
toggleMaximize,
|
||||
closeWindow,
|
||||
getMaximizedState,
|
||||
generateTitleText,
|
||||
generateFullTitleText
|
||||
} from './index';
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const {t} = useI18n();
|
||||
const route = useRoute();
|
||||
const isMaximized = ref(false);
|
||||
const tabStore = useTabStore();
|
||||
const documentStore = useDocumentStore();
|
||||
|
||||
// 计算属性用于图标,减少重复渲染
|
||||
const isMaximized = ref(false);
|
||||
const maximizeIcon = computed(() => isMaximized.value ? '' : '');
|
||||
|
||||
// 判断是否在设置页面
|
||||
const isInSettings = computed(() => route.path.startsWith('/settings'));
|
||||
|
||||
// 计算标题文本
|
||||
const titleText = computed(() => {
|
||||
if (isInSettings.value) {
|
||||
return `voidraft - ` + t('settings.title');
|
||||
}
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
if (currentDoc) {
|
||||
// 限制文档标题长度,避免标题栏换行
|
||||
const maxTitleLength = 30;
|
||||
const truncatedTitle = currentDoc.title.length > maxTitleLength
|
||||
? currentDoc.title.substring(0, maxTitleLength) + '...'
|
||||
: currentDoc.title;
|
||||
return `voidraft - ${truncatedTitle}`;
|
||||
}
|
||||
return 'voidraft';
|
||||
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
|
||||
return generateTitleText(documentStore.currentDocument?.title);
|
||||
});
|
||||
|
||||
// 计算完整标题文本(用于tooltip)
|
||||
const fullTitleText = computed(() => {
|
||||
if (isInSettings.value) {
|
||||
return `voidraft - ` + t('settings.title');
|
||||
}
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
|
||||
return generateFullTitleText(documentStore.currentDocument?.title);
|
||||
});
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
await runtime.Window.ToggleMaximise();
|
||||
await checkMaximizedState();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
const handleToggleMaximize = async () => {
|
||||
await toggleMaximize();
|
||||
isMaximized.value = await getMaximizedState();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
isMaximized.value = await getMaximizedState();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -152,7 +114,7 @@ onMounted(async () => {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
cursor: default;
|
||||
min-width: 0; /* 允许内容收缩 */
|
||||
min-width: 0;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
@@ -178,7 +140,6 @@ onMounted(async () => {
|
||||
overflow: hidden;
|
||||
margin-left: 8px;
|
||||
min-width: 0;
|
||||
//margin-right: 8px;
|
||||
}
|
||||
|
||||
.titlebar-controls {
|
||||
|
||||
60
frontend/src/components/titlebar/index.ts
Normal file
60
frontend/src/components/titlebar/index.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
|
||||
/**
|
||||
* Titlebar utility functions
|
||||
*/
|
||||
|
||||
// Window control functions
|
||||
export const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
console.error('Failed to minimize window:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleMaximize = async () => {
|
||||
try {
|
||||
await runtime.Window.ToggleMaximise();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle maximize:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
console.error('Failed to close window:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMaximizedState = async (): Promise<boolean> => {
|
||||
try {
|
||||
return await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
console.error('Failed to check maximized state:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate title text with optional truncation
|
||||
*/
|
||||
export const generateTitleText = (
|
||||
title: string | undefined,
|
||||
maxLength: number = 30
|
||||
): string => {
|
||||
if (!title) return 'voidraft';
|
||||
const truncated = title.length > maxLength
|
||||
? title.substring(0, maxLength) + '...'
|
||||
: title;
|
||||
return `voidraft - ${truncated}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate full title text (no truncation)
|
||||
*/
|
||||
export const generateFullTitleText = (title: string | undefined): string => {
|
||||
return title ? `voidraft - ${title}` : 'voidraft';
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
|
||||
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
|
||||
|
||||
const { t } = useI18n();
|
||||
const editorStore = readonly(useEditorStore());
|
||||
const editorStore = useEditorStore();
|
||||
|
||||
// 组件状态
|
||||
const showLanguageMenu = shallowRef(false);
|
||||
|
||||
@@ -1,49 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
import { useTabStore } from '@/stores/tabStore';
|
||||
import { useWindowStore } from '@/stores/windowStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { Document } from '@/../bindings/voidraft/internal/models/models';
|
||||
import {computed, nextTick, reactive, ref, watch} from 'vue';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
import {useWindowStore} from '@/stores/windowStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {useConfirm} from '@/composables';
|
||||
import {validateDocumentTitle} from '@/common/utils/validation';
|
||||
import {formatDateTime, truncateString} from '@/common/utils/formatter';
|
||||
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models';
|
||||
|
||||
// 类型定义
|
||||
interface DocumentItem extends Document {
|
||||
isCreateOption?: boolean;
|
||||
}
|
||||
|
||||
const documentStore = useDocumentStore();
|
||||
const tabStore = useTabStore();
|
||||
const windowStore = useWindowStore();
|
||||
const { t } = useI18n();
|
||||
const {t} = useI18n();
|
||||
|
||||
// DOM 引用
|
||||
const inputRef = ref<HTMLInputElement>();
|
||||
const editInputRef = ref<HTMLInputElement>();
|
||||
|
||||
// 组件状态
|
||||
const inputValue = ref('');
|
||||
const inputRef = ref<HTMLInputElement>();
|
||||
const editingId = ref<number | null>(null);
|
||||
const editingTitle = ref('');
|
||||
const editInputRef = ref<HTMLInputElement>();
|
||||
const deleteConfirmId = ref<number | null>(null);
|
||||
const state = reactive({
|
||||
isLoaded: false,
|
||||
searchQuery: '',
|
||||
editing: {
|
||||
id: null as number | null,
|
||||
title: ''
|
||||
}
|
||||
});
|
||||
|
||||
// 常量
|
||||
const MAX_TITLE_LENGTH = 50;
|
||||
const DELETE_CONFIRM_TIMEOUT = 3000;
|
||||
|
||||
// 计算属性
|
||||
const currentDocName = computed(() => {
|
||||
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
|
||||
const title = documentStore.currentDocument.title;
|
||||
return title.length > 12 ? title.substring(0, 12) + '...' : title;
|
||||
return truncateString(documentStore.currentDocument.title || '', 12);
|
||||
});
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
const filteredItems = computed<DocumentItem[]>(() => {
|
||||
const docs = documentStore.documentList;
|
||||
const query = inputValue.value.trim();
|
||||
const query = state.searchQuery.trim();
|
||||
|
||||
if (!query) return docs;
|
||||
|
||||
const filtered = docs.filter(doc =>
|
||||
doc.title.toLowerCase().includes(query.toLowerCase())
|
||||
(doc.title || '').toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
// 如果输入的不是已存在文档的完整标题,添加创建选项
|
||||
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase());
|
||||
const exactMatch = docs.some(doc => (doc.title || '').toLowerCase() === query.toLowerCase());
|
||||
if (!exactMatch && query.length > 0) {
|
||||
return [
|
||||
{ id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true } as any,
|
||||
{id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as DocumentItem,
|
||||
...filtered
|
||||
];
|
||||
}
|
||||
@@ -51,53 +65,32 @@ const filteredItems = computed(() => {
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
const validateTitle = (title: string): string | null => {
|
||||
if (!title.trim()) return t('toolbar.documentNameRequired');
|
||||
if (title.trim().length > MAX_TITLE_LENGTH) {
|
||||
return t('toolbar.documentNameTooLong', { max: MAX_TITLE_LENGTH });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const formatTime = (dateString: string | null) => {
|
||||
if (!dateString) return t('toolbar.unknownTime');
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
|
||||
|
||||
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
|
||||
return date.toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} catch {
|
||||
return t('toolbar.timeError');
|
||||
}
|
||||
};
|
||||
|
||||
// 核心操作
|
||||
const openMenu = async () => {
|
||||
documentStore.openDocumentSelector();
|
||||
await documentStore.getDocumentMetaList();
|
||||
documentStore.openDocumentSelector();
|
||||
state.isLoaded = true;
|
||||
await nextTick();
|
||||
inputRef.value?.focus();
|
||||
};
|
||||
|
||||
// 删除确认
|
||||
const {isConfirming: isDeleting, startConfirm: startDeleteConfirm, reset: resetDeleteConfirm} = useConfirm({
|
||||
timeout: DELETE_CONFIRM_TIMEOUT
|
||||
});
|
||||
|
||||
const closeMenu = () => {
|
||||
state.isLoaded = false;
|
||||
documentStore.closeDocumentSelector();
|
||||
inputValue.value = '';
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
deleteConfirmId.value = null;
|
||||
state.searchQuery = '';
|
||||
state.editing.id = null;
|
||||
state.editing.title = '';
|
||||
resetDeleteConfirm();
|
||||
};
|
||||
|
||||
const selectDoc = async (doc: Document) => {
|
||||
if (doc.id === undefined) return;
|
||||
|
||||
// 如果选择的就是当前文档,直接关闭菜单
|
||||
if (documentStore.currentDocument?.id === doc.id) {
|
||||
closeMenu();
|
||||
@@ -121,7 +114,7 @@ const selectDoc = async (doc: Document) => {
|
||||
|
||||
const createDoc = async (title: string) => {
|
||||
const trimmedTitle = title.trim();
|
||||
const error = validateTitle(trimmedTitle);
|
||||
const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
|
||||
if (error) return;
|
||||
|
||||
try {
|
||||
@@ -132,20 +125,28 @@ const createDoc = async (title: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = async (item: any) => {
|
||||
const selectDocItem = async (item: any) => {
|
||||
if (item.isCreateOption) {
|
||||
await createDoc(inputValue.value.trim());
|
||||
await createDoc(state.searchQuery.trim());
|
||||
} else {
|
||||
await selectDoc(item);
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索框回车处理
|
||||
const handleSearchEnter = () => {
|
||||
const query = state.searchQuery.trim();
|
||||
if (query && filteredItems.value.length > 0) {
|
||||
selectDocItem(filteredItems.value[0]);
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑操作
|
||||
const startRename = (doc: Document, event: Event) => {
|
||||
const renameDoc = (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
editingId.value = doc.id;
|
||||
editingTitle.value = doc.title;
|
||||
deleteConfirmId.value = null;
|
||||
state.editing.id = doc.id ?? null;
|
||||
state.editing.title = doc.title || '';
|
||||
resetDeleteConfirm();
|
||||
nextTick(() => {
|
||||
editInputRef.value?.focus();
|
||||
editInputRef.value?.select();
|
||||
@@ -153,35 +154,41 @@ const startRename = (doc: Document, event: Event) => {
|
||||
};
|
||||
|
||||
const saveEdit = async () => {
|
||||
if (!editingId.value || !editingTitle.value.trim()) {
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
if (!state.editing.id || !state.editing.title.trim()) {
|
||||
state.editing.id = null;
|
||||
state.editing.title = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedTitle = editingTitle.value.trim();
|
||||
const error = validateTitle(trimmedTitle);
|
||||
const trimmedTitle = state.editing.title.trim();
|
||||
const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
|
||||
if (error) return;
|
||||
|
||||
try {
|
||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||
await documentStore.updateDocumentMetadata(state.editing.id, trimmedTitle);
|
||||
await documentStore.getDocumentMetaList();
|
||||
|
||||
// 如果tabs功能开启且该文档有标签页,更新标签页标题
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(editingId.value)) {
|
||||
tabStore.updateTabTitle(editingId.value, trimmedTitle);
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) {
|
||||
tabStore.updateTabTitle(state.editing.id, trimmedTitle);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update document:', error);
|
||||
} finally {
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
state.editing.id = null;
|
||||
state.editing.title = '';
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
state.editing.id = null;
|
||||
state.editing.title = '';
|
||||
};
|
||||
|
||||
// 其他操作
|
||||
const openInNewWindow = async (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
if (doc.id === undefined) return;
|
||||
try {
|
||||
await documentStore.openDocumentInNewWindow(doc.id);
|
||||
} catch (error) {
|
||||
@@ -191,13 +198,14 @@ const openInNewWindow = async (doc: Document, event: Event) => {
|
||||
|
||||
const handleDelete = async (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
if (doc.id === undefined) return;
|
||||
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
if (isDeleting(doc.id)) {
|
||||
// 确认删除前检查文档是否在其他窗口打开
|
||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||
if (hasOpen) {
|
||||
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
|
||||
deleteConfirmId.value = null;
|
||||
resetDeleteConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,115 +218,60 @@ const handleDelete = async (doc: Document, event: Event) => {
|
||||
if (firstDoc) await selectDoc(firstDoc);
|
||||
}
|
||||
}
|
||||
deleteConfirmId.value = null;
|
||||
resetDeleteConfirm();
|
||||
} else {
|
||||
// 进入确认状态
|
||||
deleteConfirmId.value = doc.id;
|
||||
editingId.value = null;
|
||||
|
||||
// 3秒后自动取消确认状态
|
||||
setTimeout(() => {
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
deleteConfirmId.value = null;
|
||||
}
|
||||
}, 3000);
|
||||
startDeleteConfirm(doc.id);
|
||||
state.editing.id = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件处理
|
||||
const createKeyHandler = (handlers: Record<string, () => void>) => (event: KeyboardEvent) => {
|
||||
const handler = handlers[event.key];
|
||||
if (handler) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGlobalKeydown = createKeyHandler({
|
||||
Escape: () => {
|
||||
if (editingId.value) {
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
} else if (deleteConfirmId.value) {
|
||||
deleteConfirmId.value = null;
|
||||
// 切换菜单
|
||||
const toggleMenu = () => {
|
||||
if (documentStore.showDocumentSelector) {
|
||||
closeMenu();
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleInputKeydown = createKeyHandler({
|
||||
Enter: () => {
|
||||
const query = inputValue.value.trim();
|
||||
if (query && filteredItems.value.length > 0) {
|
||||
selectItem(filteredItems.value[0]);
|
||||
}
|
||||
},
|
||||
Escape: closeMenu
|
||||
});
|
||||
|
||||
const handleEditKeydown = createKeyHandler({
|
||||
Enter: saveEdit,
|
||||
Escape: () => {
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 点击外部关闭
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.document-selector')) {
|
||||
closeMenu();
|
||||
openMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleGlobalKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleGlobalKeydown);
|
||||
});
|
||||
|
||||
// 监听菜单状态变化
|
||||
watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
if (isOpen) {
|
||||
if (isOpen && !state.isLoaded) {
|
||||
openMenu();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-selector">
|
||||
<div class="document-selector" v-click-outside="closeMenu">
|
||||
<!-- 选择器按钮 -->
|
||||
<button class="doc-btn" @click="documentStore.toggleDocumentSelector">
|
||||
<button class="doc-btn" @click="toggleMenu">
|
||||
<span class="doc-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
||||
<polyline points="14,2 14,8 20,8"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="doc-name">{{ currentDocName }}</span>
|
||||
<span class="arrow" :class="{ open: documentStore.showDocumentSelector }">▲</span>
|
||||
<span class="arrow" :class="{ open: state.isLoaded }">▲</span>
|
||||
</button>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<div v-if="documentStore.showDocumentSelector" class="doc-menu">
|
||||
<Transition name="slide-up">
|
||||
<div v-if="state.isLoaded" class="doc-menu">
|
||||
<!-- 输入框 -->
|
||||
<div class="input-box">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
v-model="state.searchQuery"
|
||||
type="text"
|
||||
class="main-input"
|
||||
:placeholder="t('toolbar.searchOrCreateDocument')"
|
||||
:maxlength="MAX_TITLE_LENGTH"
|
||||
@keydown="handleInputKeydown"
|
||||
@keydown.enter="handleSearchEnter"
|
||||
@keydown.esc="closeMenu"
|
||||
/>
|
||||
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -337,7 +290,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
|
||||
'create-item': item.isCreateOption
|
||||
}"
|
||||
@click="selectItem(item)"
|
||||
@click="selectDocItem(item)"
|
||||
>
|
||||
<!-- 创建选项 -->
|
||||
<div v-if="item.isCreateOption" class="create-option">
|
||||
@@ -352,31 +305,32 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
<!-- 文档项 -->
|
||||
<div v-else class="doc-item-content">
|
||||
<!-- 普通显示 -->
|
||||
<div v-if="editingId !== item.id" class="doc-info">
|
||||
<div v-if="state.editing.id !== item.id" class="doc-info">
|
||||
<div class="doc-title">{{ item.title }}</div>
|
||||
<!-- 根据状态显示错误信息或时间 -->
|
||||
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
|
||||
{{ documentStore.selectorError?.message }}
|
||||
</div>
|
||||
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
||||
<div v-else class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑状态 -->
|
||||
<div v-else class="doc-edit">
|
||||
<input
|
||||
:ref="el => editInputRef = el as HTMLInputElement"
|
||||
v-model="editingTitle"
|
||||
v-model="state.editing.title"
|
||||
type="text"
|
||||
class="edit-input"
|
||||
:maxlength="MAX_TITLE_LENGTH"
|
||||
@keydown="handleEditKeydown"
|
||||
@keydown.enter="saveEdit"
|
||||
@keydown.esc="cancelEdit"
|
||||
@blur="saveEdit"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="editingId !== item.id" class="doc-actions">
|
||||
<div v-if="state.editing.id !== item.id" class="doc-actions">
|
||||
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
|
||||
<button
|
||||
v-if="documentStore.currentDocument?.id !== item.id"
|
||||
@@ -392,7 +346,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')">
|
||||
<button class="action-btn" @click="renameDoc(item, $event)" :title="t('toolbar.rename')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
|
||||
@@ -401,11 +355,11 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
<button
|
||||
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
||||
class="action-btn delete-btn"
|
||||
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
|
||||
:class="{ 'delete-confirm': isDeleting(item.id!) }"
|
||||
@click="handleDelete(item, $event)"
|
||||
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
|
||||
:title="isDeleting(item.id!) ? t('toolbar.confirmDelete') : t('toolbar.delete')"
|
||||
>
|
||||
<svg v-if="deleteConfirmId !== item.id" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
|
||||
<svg v-if="!isDeleting(item.id!)" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<polyline points="3,6 5,6 21,6"></polyline>
|
||||
@@ -421,17 +375,24 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
<div v-if="filteredItems.length === 0" class="empty">
|
||||
{{ t('toolbar.noDocumentFound') }}
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="documentStore.isLoading" class="loading">
|
||||
{{ t('toolbar.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.document-selector {
|
||||
position: relative;
|
||||
|
||||
@@ -483,8 +444,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
width: 260px;
|
||||
max-height: calc(100vh - 40px); // 限制最大高度,留出titlebar空间(32px)和一些边距
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 40px);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
@@ -527,7 +488,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
}
|
||||
|
||||
.item-list {
|
||||
max-height: calc(100vh - 100px); // 为输入框和边距预留空间
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
@@ -669,7 +630,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
}
|
||||
}
|
||||
|
||||
.empty, .loading {
|
||||
.empty {
|
||||
padding: 16px 8px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
@@ -680,9 +641,17 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0% { opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,11 +14,11 @@ import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/langu
|
||||
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
const editorStore = readonly(useEditorStore());
|
||||
const configStore = readonly(useConfigStore());
|
||||
const updateStore = readonly(useUpdateStore());
|
||||
const windowStore = readonly(useWindowStore());
|
||||
const systemStore = readonly(useSystemStore());
|
||||
const editorStore = useEditorStore();
|
||||
const configStore = useConfigStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const windowStore = useWindowStore();
|
||||
const systemStore = useSystemStore();
|
||||
const {t} = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -33,6 +33,7 @@ const isCurrentWindowOnTop = computed(() => {
|
||||
return config.value.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||
});
|
||||
|
||||
|
||||
// 切换窗口置顶状态
|
||||
const toggleAlwaysOnTop = async () => {
|
||||
const currentlyOnTop = isCurrentWindowOnTop.value;
|
||||
@@ -60,9 +61,10 @@ const formatCurrentBlock = () => {
|
||||
formatBlockContent(editorStore.editorView);
|
||||
};
|
||||
|
||||
// 格式化按钮状态更新 - 使用更高效的检查逻辑
|
||||
const updateFormatButtonState = () => {
|
||||
const view = editorStore.editorView;
|
||||
|
||||
// 统一更新按钮状态
|
||||
const updateButtonStates = () => {
|
||||
const view: any = editorStore.editorView;
|
||||
if (!view) {
|
||||
canFormatCurrentBlock.value = false;
|
||||
return;
|
||||
@@ -78,17 +80,19 @@ const updateFormatButtonState = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const language = getLanguage(activeBlock.language.name as any);
|
||||
const languageName = activeBlock.language.name;
|
||||
const language = getLanguage(languageName as any);
|
||||
|
||||
canFormatCurrentBlock.value = Boolean(language?.prettier);
|
||||
} catch (error) {
|
||||
console.warn('Error checking format capability:', error);
|
||||
console.warn('Error checking block capabilities:', error);
|
||||
canFormatCurrentBlock.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建带1s防抖的更新函数
|
||||
const { debouncedFn: debouncedUpdateFormat, cancel: cancelDebounce } = createDebounce(
|
||||
updateFormatButtonState,
|
||||
const { debouncedFn: debouncedUpdateButtonStates, cancel: cancelDebounce } = createDebounce(
|
||||
updateButtonStates,
|
||||
{ delay: 1000 }
|
||||
);
|
||||
|
||||
@@ -102,9 +106,9 @@ const setupEditorListeners = (view: any) => {
|
||||
|
||||
// 使用对象缓存事件处理器,避免重复创建
|
||||
const eventHandlers = {
|
||||
click: updateFormatButtonState,
|
||||
keyup: debouncedUpdateFormat,
|
||||
focus: updateFormatButtonState
|
||||
click: updateButtonStates,
|
||||
keyup: debouncedUpdateButtonStates,
|
||||
focus: updateButtonStates
|
||||
} as const;
|
||||
|
||||
const events = Object.entries(eventHandlers).map(([type, handler]) => ({
|
||||
@@ -131,7 +135,7 @@ watch(
|
||||
|
||||
if (newView) {
|
||||
// 初始更新状态
|
||||
updateFormatButtonState();
|
||||
updateButtonStates();
|
||||
// 设置新监听器
|
||||
cleanupListeners = setupEditorListeners(newView);
|
||||
} else {
|
||||
@@ -145,8 +149,8 @@ watch(
|
||||
// 组件生命周期
|
||||
onMounted(async () => {
|
||||
isLoaded.value = true;
|
||||
// 首次更新格式化状态
|
||||
updateFormatButtonState();
|
||||
// 首次更新按钮状态
|
||||
updateButtonStates();
|
||||
await systemStore.setWindowOnTop(isCurrentWindowOnTop.value);
|
||||
});
|
||||
|
||||
@@ -512,6 +516,42 @@ const statsData = computed(() => ({
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(100, 149, 237, 0.2);
|
||||
|
||||
svg {
|
||||
stroke: #6495ed;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: var(--text-muted);
|
||||
transition: stroke 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
stroke: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
5
frontend/src/composables/index.ts
Normal file
5
frontend/src/composables/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { useConfirm } from './useConfirm';
|
||||
export type { UseConfirmOptions } from './useConfirm';
|
||||
|
||||
export { usePolling } from './usePolling';
|
||||
export type { UsePollingOptions, UsePollingReturn } from './usePolling';
|
||||
174
frontend/src/composables/useConfirm.ts
Normal file
174
frontend/src/composables/useConfirm.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
|
||||
|
||||
export interface UseConfirmOptions<T extends string | number = string | number> {
|
||||
/** Auto cancel timeout in ms (default: 3000, set 0 to disable) */
|
||||
timeout?: number;
|
||||
/** Callback when confirmed */
|
||||
onConfirm?: (id: T) => void | Promise<void>;
|
||||
/** Callback when cancelled (timeout or manual) */
|
||||
onCancel?: (id: T) => void;
|
||||
}
|
||||
|
||||
export interface UseConfirmReturn<T extends string | number = string | number> {
|
||||
/** Current confirming id (readonly) */
|
||||
confirmId: DeepReadonly<Ref<T | null>>;
|
||||
/** Whether confirm action is executing */
|
||||
isPending: DeepReadonly<Ref<boolean>>;
|
||||
/** Check if a specific id is in confirming state */
|
||||
isConfirming: (id: T) => boolean;
|
||||
/** Start confirming state (with auto timeout) */
|
||||
startConfirm: (id: T) => void;
|
||||
/** Request confirmation (toggle between request and execute) */
|
||||
requestConfirm: (id: T) => Promise<boolean>;
|
||||
/** Manually confirm current id */
|
||||
confirm: () => Promise<void>;
|
||||
/** Cancel confirmation */
|
||||
cancel: () => void;
|
||||
/** Reset without triggering callbacks */
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for handling confirm actions (e.g., delete confirmation)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Basic usage
|
||||
* const { isConfirming, requestConfirm } = useConfirm({
|
||||
* timeout: 3000,
|
||||
* onConfirm: async (id) => { await deleteItem(id) }
|
||||
* })
|
||||
*
|
||||
* // In template
|
||||
* <button @click="requestConfirm('delete')">
|
||||
* {{ isConfirming('delete') ? 'Confirm?' : 'Delete' }}
|
||||
* </button>
|
||||
*
|
||||
* // With loading state
|
||||
* const { isPending, requestConfirm } = useConfirm({ ... })
|
||||
* <button :disabled="isPending" @click="requestConfirm('id')">
|
||||
* {{ isPending ? 'Processing...' : 'Delete' }}
|
||||
* </button>
|
||||
* ```
|
||||
*/
|
||||
export function useConfirm<T extends string | number = string | number>(
|
||||
options: UseConfirmOptions<T> = {}
|
||||
): UseConfirmReturn<T> {
|
||||
const { timeout = 3000, onConfirm, onCancel } = options;
|
||||
|
||||
const confirmId = ref<T | null>(null) as Ref<T | null>;
|
||||
const isPending = ref(false);
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearTimer = (): void => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a specific id is in confirming state
|
||||
*/
|
||||
const isConfirming = (id: T): boolean => {
|
||||
return confirmId.value === id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Start confirming state for an id (with auto timeout)
|
||||
*/
|
||||
const startConfirm = (id: T): void => {
|
||||
clearTimer();
|
||||
confirmId.value = id;
|
||||
|
||||
// Auto cancel after timeout (0 = disabled)
|
||||
if (timeout > 0) {
|
||||
timeoutId = setTimeout(() => {
|
||||
if (confirmId.value === id) {
|
||||
confirmId.value = null;
|
||||
onCancel?.(id);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request confirmation for an id
|
||||
* - First click: enter confirming state
|
||||
* - Second click: execute confirm action
|
||||
* @returns true if confirmed, false if entered confirming state
|
||||
*/
|
||||
const requestConfirm = async (id: T): Promise<boolean> => {
|
||||
// Prevent action while pending
|
||||
if (isPending.value) return false;
|
||||
|
||||
if (confirmId.value === id) {
|
||||
// Already confirming, execute action
|
||||
clearTimer();
|
||||
isPending.value = true;
|
||||
try {
|
||||
await onConfirm?.(id);
|
||||
return true;
|
||||
} finally {
|
||||
confirmId.value = null;
|
||||
isPending.value = false;
|
||||
}
|
||||
} else {
|
||||
// Enter confirming state
|
||||
startConfirm(id);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually confirm the current id
|
||||
*/
|
||||
const confirm = async (): Promise<void> => {
|
||||
if (confirmId.value === null || isPending.value) return;
|
||||
|
||||
clearTimer();
|
||||
const id = confirmId.value;
|
||||
isPending.value = true;
|
||||
try {
|
||||
await onConfirm?.(id);
|
||||
} finally {
|
||||
confirmId.value = null;
|
||||
isPending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the confirming state
|
||||
*/
|
||||
const cancel = (): void => {
|
||||
if (confirmId.value === null) return;
|
||||
|
||||
const id = confirmId.value;
|
||||
clearTimer();
|
||||
confirmId.value = null;
|
||||
onCancel?.(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset state without triggering callbacks
|
||||
*/
|
||||
const reset = (): void => {
|
||||
clearTimer();
|
||||
confirmId.value = null;
|
||||
isPending.value = false;
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(clearTimer);
|
||||
|
||||
return {
|
||||
confirmId: readonly(confirmId),
|
||||
isPending: readonly(isPending),
|
||||
isConfirming,
|
||||
startConfirm,
|
||||
requestConfirm,
|
||||
confirm,
|
||||
cancel,
|
||||
reset
|
||||
};
|
||||
}
|
||||
147
frontend/src/composables/usePolling.ts
Normal file
147
frontend/src/composables/usePolling.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
|
||||
|
||||
export interface UsePollingOptions<T> {
|
||||
/** Polling interval in ms (default: 500) */
|
||||
interval?: number;
|
||||
/** Execute immediately when started (default: true) */
|
||||
immediate?: boolean;
|
||||
/** Auto-stop condition, return true to stop polling */
|
||||
shouldStop?: (data: T) => boolean;
|
||||
/** Callback on each successful poll */
|
||||
onSuccess?: (data: T) => void;
|
||||
/** Callback when error occurs */
|
||||
onError?: (error: unknown) => void;
|
||||
/** Callback when polling stops (either manual or auto) */
|
||||
onStop?: () => void;
|
||||
}
|
||||
|
||||
export interface UsePollingReturn<T> {
|
||||
/** Latest fetched data (readonly) */
|
||||
data: DeepReadonly<Ref<T | null>>;
|
||||
/** Error message if any (readonly) */
|
||||
error: DeepReadonly<Ref<string>>;
|
||||
/** Whether polling is active (readonly) */
|
||||
isActive: DeepReadonly<Ref<boolean>>;
|
||||
/** Start polling */
|
||||
start: () => void;
|
||||
/** Stop polling */
|
||||
stop: () => void;
|
||||
/** Reset all state (also stops polling) */
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for polling async operations with auto-stop support
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Basic usage
|
||||
* const { data, isActive, start, stop } = usePolling(
|
||||
* () => api.getProgress(),
|
||||
* {
|
||||
* interval: 200,
|
||||
* shouldStop: (d) => d.progress >= 100 || !!d.error,
|
||||
* onSuccess: (d) => console.log('Progress:', d.progress)
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* // Start polling
|
||||
* start()
|
||||
*
|
||||
* // With reactive data binding
|
||||
* <div>{{ isActive ? `${data?.progress}%` : 'Idle' }}</div>
|
||||
* ```
|
||||
*/
|
||||
export function usePolling<T>(
|
||||
fetcher: () => Promise<T>,
|
||||
options: UsePollingOptions<T> = {}
|
||||
): UsePollingReturn<T> {
|
||||
const {
|
||||
interval = 500,
|
||||
immediate = true,
|
||||
shouldStop,
|
||||
onSuccess,
|
||||
onError,
|
||||
onStop
|
||||
} = options;
|
||||
|
||||
const data = ref<T | null>(null) as Ref<T | null>;
|
||||
const error = ref('');
|
||||
const isActive = ref(false);
|
||||
let timerId = 0;
|
||||
|
||||
const clearTimer = (): void => {
|
||||
if (timerId) {
|
||||
clearInterval(timerId);
|
||||
timerId = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const poll = async (): Promise<void> => {
|
||||
try {
|
||||
const result = await fetcher();
|
||||
data.value = result;
|
||||
error.value = '';
|
||||
onSuccess?.(result);
|
||||
|
||||
// Check auto-stop condition
|
||||
if (shouldStop?.(result)) {
|
||||
stop();
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
onError?.(e);
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start polling
|
||||
*/
|
||||
const start = (): void => {
|
||||
if (isActive.value) return;
|
||||
|
||||
isActive.value = true;
|
||||
error.value = '';
|
||||
|
||||
// Execute immediately if configured
|
||||
if (immediate) {
|
||||
poll();
|
||||
}
|
||||
|
||||
timerId = window.setInterval(poll, interval);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop polling
|
||||
*/
|
||||
const stop = (): void => {
|
||||
if (!isActive.value) return;
|
||||
|
||||
clearTimer();
|
||||
isActive.value = false;
|
||||
onStop?.();
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset all state to initial values
|
||||
*/
|
||||
const reset = (): void => {
|
||||
clearTimer();
|
||||
data.value = null;
|
||||
error.value = '';
|
||||
isActive.value = false;
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(clearTimer);
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
isActive: readonly(isActive),
|
||||
start,
|
||||
stop,
|
||||
reset
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user