chore: migrate and structure cmbone project
@@ -1,27 +1,23 @@
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
.task/
|
||||
bin/
|
||||
dist/
|
||||
node_modules/
|
||||
frontend/dist/
|
||||
frontend/node_modules/
|
||||
frontend/.vite/
|
||||
frontend/coverage/
|
||||
build/linux/appimage/build/
|
||||
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
.DS_Store
|
||||
*.log
|
||||
*.local
|
||||
*.syso
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
.env.*
|
||||
!.env.example
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 JinGong
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,3 +1,64 @@
|
||||
# cmbone
|
||||
|
||||
桌面端骨架
|
||||
基于 SuiDemo 迁移而来的 Wails v3 桌面项目,已从模板仓库整理为可直接维护和构建的普通工程。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 后端:Go 1.23.0、Wails v3.0.0-alpha.9、SQLite
|
||||
- 前端:Vue 3、TypeScript、Vite、Tailwind CSS、Ant Design Vue
|
||||
- 功能:多语言、主题切换、本地配置、快捷键、OCR 示例
|
||||
|
||||
## 环境准备
|
||||
|
||||
当前项目按本机 Go 1.23.0 锁定依赖,不要直接安装 `wails3@latest`。
|
||||
|
||||
```powershell
|
||||
go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha.9
|
||||
```
|
||||
|
||||
如果安装后 PowerShell 找不到 `wails3`,把 Go bin 目录加入当前终端 PATH:
|
||||
|
||||
```powershell
|
||||
$env:Path += ";$(go env GOPATH)\bin"
|
||||
wails3 doctor
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
.
|
||||
├── main.go # Wails 应用入口
|
||||
├── internal/
|
||||
│ ├── models/ # 后端数据模型
|
||||
│ └── services/ # 后端服务和数据库逻辑
|
||||
├── platform/ # Windows/macOS 平台差异代码
|
||||
├── frontend/
|
||||
│ ├── src/ # Vue 前端源码
|
||||
│ ├── bindings/ # Wails 前端绑定
|
||||
│ └── dist/ # 前端构建产物,供 Go embed 使用
|
||||
└── build/ # Wails 构建配置
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
安装 `wails3` 后可以使用:
|
||||
|
||||
```bash
|
||||
wails3 dev
|
||||
wails3 package
|
||||
```
|
||||
|
||||
## 维护约定
|
||||
|
||||
- Go 模块名统一为 `cmbone`。
|
||||
- 当前依赖按系统 Go 1.23.0 锁定,升级 Go 前不要随意升级 Wails、SQLite 或热键库。
|
||||
- 修改后端服务后,优先使用 `wails3 generate bindings` 重新生成 `frontend/bindings`;如果本机没有 CLI,只做小范围手动绑定。
|
||||
- 提交前至少运行 `go test ./...` 和 `npm run build`。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ./build/Taskfile.yml
|
||||
windows: ./build/windows/Taskfile.yml
|
||||
darwin: ./build/darwin/Taskfile.yml
|
||||
linux: ./build/linux/Taskfile.yml
|
||||
|
||||
vars:
|
||||
APP_NAME: cmbone
|
||||
BIN_DIR: bin
|
||||
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Builds the application
|
||||
cmds:
|
||||
- task: '{{OS}}:build'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application
|
||||
cmds:
|
||||
- task: '{{OS}}:package'
|
||||
|
||||
run:
|
||||
summary: Runs the application
|
||||
cmds:
|
||||
- task: '{{OS}}:run'
|
||||
|
||||
dev:
|
||||
summary: Runs the application in development mode
|
||||
cmds:
|
||||
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||
@@ -0,0 +1,85 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
go:mod:tidy:
|
||||
summary: Runs `go mod tidy`
|
||||
internal: true
|
||||
cmds:
|
||||
- go mod tidy
|
||||
|
||||
install:frontend:deps:
|
||||
summary: Install frontend dependencies
|
||||
dir: frontend
|
||||
sources:
|
||||
- package.json
|
||||
- package-lock.json
|
||||
generates:
|
||||
- node_modules/*
|
||||
preconditions:
|
||||
- sh: npm version
|
||||
msg: "npm is required. Install it with Node.js."
|
||||
cmds:
|
||||
- npm install
|
||||
|
||||
build:frontend:
|
||||
label: build:frontend (PRODUCTION={{.PRODUCTION}})
|
||||
summary: Build the frontend project
|
||||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
- task: install:frontend:deps
|
||||
- task: generate:bindings
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
cmds:
|
||||
- npm run {{.BUILD_COMMAND}} -q
|
||||
env:
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
vars:
|
||||
BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}'
|
||||
|
||||
generate:bindings:
|
||||
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
|
||||
summary: Generates bindings for the frontend
|
||||
deps:
|
||||
- task: go:mod:tidy
|
||||
sources:
|
||||
- "**/*.[jt]s"
|
||||
- exclude: frontend/**/*
|
||||
- frontend/bindings/**/*
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- frontend/bindings/**/*
|
||||
cmds:
|
||||
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true {{if .Typescript}} -ts{{end}}
|
||||
|
||||
generate:icons:
|
||||
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
||||
dir: build
|
||||
sources:
|
||||
- appicon.png
|
||||
generates:
|
||||
- darwin/icons.icns
|
||||
- windows/icon.ico
|
||||
cmds:
|
||||
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
|
||||
|
||||
dev:frontend:
|
||||
summary: Runs the frontend in development mode
|
||||
dir: frontend
|
||||
deps:
|
||||
- task: install:frontend:deps
|
||||
cmds:
|
||||
- npm run dev -- --port {{.VITE_PORT}} --strictPort
|
||||
|
||||
update:build-assets:
|
||||
summary: Updates the build assets
|
||||
dir: build
|
||||
cmds:
|
||||
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
|
||||
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,63 @@
|
||||
# This file contains the configuration for this project.
|
||||
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
|
||||
# Note that this will overwrite any changes you have made to the assets.
|
||||
version: '3'
|
||||
|
||||
# This information is used to generate the build assets.
|
||||
info:
|
||||
companyName: "cmbone"
|
||||
productName: "cmbone"
|
||||
productIdentifier: "com.cmbone.app"
|
||||
description: "A Wails desktop application"
|
||||
copyright: "(c) 2026, cmbone"
|
||||
comments: "cmbone desktop application"
|
||||
version: "0.0.1" # The application version
|
||||
|
||||
# Dev mode configuration
|
||||
dev_mode:
|
||||
root_path: .
|
||||
log_level: warn
|
||||
debounce: 1000
|
||||
ignore:
|
||||
dir:
|
||||
- .git
|
||||
- node_modules
|
||||
- frontend
|
||||
- bin
|
||||
file:
|
||||
- .DS_Store
|
||||
- .gitignore
|
||||
- .gitkeep
|
||||
watched_extension:
|
||||
- "*.go"
|
||||
git_ignore: true
|
||||
executes:
|
||||
- cmd: wails3 task common:install:frontend:deps
|
||||
type: once
|
||||
- cmd: wails3 task common:dev:frontend
|
||||
type: background
|
||||
- cmd: go mod tidy
|
||||
type: blocking
|
||||
- cmd: wails3 task build
|
||||
type: blocking
|
||||
- cmd: wails3 task run
|
||||
type: primary
|
||||
|
||||
# File Associations
|
||||
# More information at: https://v3.wails.io/noit/done/yet
|
||||
fileAssociations:
|
||||
# - ext: wails
|
||||
# name: Wails
|
||||
# description: Wails Application File
|
||||
# iconName: wailsFileIcon
|
||||
# role: Editor
|
||||
# - ext: jpg
|
||||
# name: JPEG
|
||||
# description: Image File
|
||||
# iconName: jpegFileIcon
|
||||
# role: Editor
|
||||
# mimeType: image/jpeg # (optional)
|
||||
|
||||
# Other data
|
||||
other:
|
||||
- name: My Other Data
|
||||
@@ -0,0 +1,81 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ../Taskfile.yml
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Creates a production build of the application
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||
env:
|
||||
GOOS: darwin
|
||||
CGO_ENABLED: 1
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
build:universal:
|
||||
summary: Builds darwin universal binary (arm64 + amd64)
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
ARCH: amd64
|
||||
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
|
||||
- task: build
|
||||
vars:
|
||||
ARCH: arm64
|
||||
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||
cmds:
|
||||
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application into a `.app` bundle
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: create:app:bundle
|
||||
|
||||
package:universal:
|
||||
summary: Packages darwin universal binary (arm64 + amd64)
|
||||
deps:
|
||||
- task: build:universal
|
||||
cmds:
|
||||
- task: create:app:bundle
|
||||
|
||||
|
||||
create:app:bundle:
|
||||
summary: Creates an `.app` bundle
|
||||
cmds:
|
||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
||||
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS
|
||||
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
|
||||
@@ -0,0 +1,119 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ../Taskfile.yml
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Builds the application for Linux
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
env:
|
||||
GOOS: linux
|
||||
CGO_ENABLED: 1
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application for Linux
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: create:appimage
|
||||
- task: create:deb
|
||||
- task: create:rpm
|
||||
- task: create:aur
|
||||
|
||||
create:appimage:
|
||||
summary: Creates an AppImage
|
||||
dir: build/linux/appimage
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
- task: generate:dotdesktop
|
||||
cmds:
|
||||
- cp {{.APP_BINARY}} {{.APP_NAME}}
|
||||
- cp ../../appicon.png appicon.png
|
||||
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
|
||||
vars:
|
||||
APP_NAME: '{{.APP_NAME}}'
|
||||
APP_BINARY: '../../../bin/{{.APP_NAME}}'
|
||||
ICON: '../../appicon.png'
|
||||
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
|
||||
OUTPUT_DIR: '../../../bin'
|
||||
|
||||
create:deb:
|
||||
summary: Creates a deb package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: generate:dotdesktop
|
||||
- task: generate:deb
|
||||
|
||||
create:rpm:
|
||||
summary: Creates a rpm package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: generate:dotdesktop
|
||||
- task: generate:rpm
|
||||
|
||||
create:aur:
|
||||
summary: Creates a arch linux packager package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- task: generate:dotdesktop
|
||||
- task: generate:aur
|
||||
|
||||
generate:deb:
|
||||
summary: Creates a deb package
|
||||
cmds:
|
||||
- wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||
|
||||
generate:rpm:
|
||||
summary: Creates a rpm package
|
||||
cmds:
|
||||
- wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||
|
||||
generate:aur:
|
||||
summary: Creates a arch linux packager package
|
||||
cmds:
|
||||
- wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
|
||||
|
||||
generate:dotdesktop:
|
||||
summary: Generates a `.desktop` file
|
||||
dir: build
|
||||
cmds:
|
||||
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
|
||||
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
|
||||
vars:
|
||||
APP_NAME: '{{.APP_NAME}}'
|
||||
EXEC: '{{.APP_NAME}}'
|
||||
ICON: 'appicon'
|
||||
CATEGORIES: 'Development;'
|
||||
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2018-Present Lea Anthony
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Fail script on any error
|
||||
set -euxo pipefail
|
||||
|
||||
# Define variables
|
||||
APP_DIR="${APP_NAME}.AppDir"
|
||||
|
||||
# Create AppDir structure
|
||||
mkdir -p "${APP_DIR}/usr/bin"
|
||||
cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
|
||||
cp "${ICON_PATH}" "${APP_DIR}/"
|
||||
cp "${DESKTOP_FILE}" "${APP_DIR}/"
|
||||
|
||||
if [[ $(uname -m) == *x86_64* ]]; then
|
||||
# Download linuxdeploy and make it executable
|
||||
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage
|
||||
|
||||
# Run linuxdeploy to bundle the application
|
||||
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||
else
|
||||
# Download linuxdeploy and make it executable (arm64)
|
||||
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
|
||||
chmod +x linuxdeploy-aarch64.AppImage
|
||||
|
||||
# Run linuxdeploy to bundle the application (arm64)
|
||||
./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
|
||||
fi
|
||||
|
||||
# Rename the generated AppImage
|
||||
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#!/bin/bash
|
||||
@@ -0,0 +1 @@
|
||||
#!/bin/bash
|
||||
@@ -0,0 +1 @@
|
||||
#!/bin/bash
|
||||
@@ -0,0 +1 @@
|
||||
#!/bin/bash
|
||||
@@ -0,0 +1,98 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
common: ../Taskfile.yml
|
||||
|
||||
tasks:
|
||||
build:
|
||||
summary: Builds the application for Windows
|
||||
deps:
|
||||
- task: common:go:mod:tidy
|
||||
- task: common:build:frontend
|
||||
vars:
|
||||
BUILD_FLAGS:
|
||||
ref: .BUILD_FLAGS
|
||||
PRODUCTION:
|
||||
ref: .PRODUCTION
|
||||
- task: common:generate:icons
|
||||
cmds:
|
||||
- task: generate:syso
|
||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
||||
- cmd: powershell Remove-item *.syso
|
||||
platforms: [windows]
|
||||
- cmd: rm -f *.syso
|
||||
platforms: [linux, darwin]
|
||||
vars:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
env:
|
||||
GOOS: windows
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
package:
|
||||
summary: Packages a production build of the application
|
||||
cmds:
|
||||
- |-
|
||||
if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then
|
||||
task: create:msix:package
|
||||
else
|
||||
task: create:nsis:installer
|
||||
fi
|
||||
vars:
|
||||
FORMAT: '{{.FORMAT | default "nsis"}}'
|
||||
|
||||
generate:syso:
|
||||
summary: Generates Windows `.syso` file
|
||||
dir: build
|
||||
cmds:
|
||||
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
|
||||
create:nsis:installer:
|
||||
summary: Creates an NSIS installer
|
||||
dir: build/windows/nsis
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
|
||||
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
|
||||
- makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||
|
||||
create:msix:package:
|
||||
summary: Creates an MSIX package
|
||||
deps:
|
||||
- task: build
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
cmds:
|
||||
- |-
|
||||
wails3 tool msix \
|
||||
--config "{{.ROOT_DIR}}/wails.json" \
|
||||
--name "{{.APP_NAME}}" \
|
||||
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
|
||||
--arch "{{.ARCH}}" \
|
||||
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
|
||||
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
|
||||
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
|
||||
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
|
||||
vars:
|
||||
ARCH: '{{.ARCH | default ARCH}}'
|
||||
CERT_PATH: '{{.CERT_PATH | default ""}}'
|
||||
PUBLISHER: '{{.PUBLISHER | default ""}}'
|
||||
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
||||
|
||||
install:msix:tools:
|
||||
summary: Installs tools required for MSIX packaging
|
||||
cmds:
|
||||
- wails3 tool msix-install-tools
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,112 @@
|
||||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "my-project" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "My Company" # Default "{{.ProductCompany}}"
|
||||
## !define INFO_PRODUCTNAME "My Product Name" # Default "{{.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "{{.ProductCopyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 214 KiB |
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
Hotkey
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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";
|
||||
|
||||
export class Hotkey {
|
||||
/**
|
||||
* 热键ID
|
||||
*/
|
||||
"id": number;
|
||||
|
||||
/**
|
||||
* 键码
|
||||
*/
|
||||
"keycode": number;
|
||||
|
||||
/**
|
||||
* 修饰键
|
||||
*/
|
||||
"modifiers": number;
|
||||
|
||||
/** Creates a new Hotkey instance. */
|
||||
constructor($$source: Partial<Hotkey> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = 0;
|
||||
}
|
||||
if (!("keycode" in $$source)) {
|
||||
this["keycode"] = 0;
|
||||
}
|
||||
if (!("modifiers" in $$source)) {
|
||||
this["modifiers"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Hotkey instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Hotkey {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Hotkey($$parsedSource as Partial<Hotkey>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* 根据key获取配置项
|
||||
*/
|
||||
export function GetAppConfig(key: string): $CancellablePromise<string> {
|
||||
return $Call.ByName("cmbone/internal/services.AppConfigService.GetAppConfig", key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用语言
|
||||
*/
|
||||
export function GetLanguage(): $CancellablePromise<string> {
|
||||
return $Call.ByName("cmbone/internal/services.AppConfigService.GetLanguage");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置或更新配置项
|
||||
*/
|
||||
export function SetAppConfig(key: string, value: string): $CancellablePromise<void> {
|
||||
return $Call.ByName("cmbone/internal/services.AppConfigService.SetAppConfig", key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置应用语言
|
||||
*/
|
||||
export function SetLanguage(lang: string): $CancellablePromise<void> {
|
||||
return $Call.ByName("cmbone/internal/services.AppConfigService.SetLanguage", lang);
|
||||
}
|
||||
@@ -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 { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
export function OpenSecondWindow(): $CancellablePromise<void> {
|
||||
return $Call.ByName("cmbone/internal/services.AppService.OpenSecondWindow");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新应用语言设置并刷新菜单显示
|
||||
*/
|
||||
export function SetLanguage(lang: string): $CancellablePromise<void> {
|
||||
return $Call.ByName("cmbone/internal/services.AppService.SetLanguage", lang);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
export function GetHotkeys(): $CancellablePromise<models$0.Hotkey[]> {
|
||||
return $Call.ByName("cmbone/internal/services.HotkeyService.GetHotkeys").then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷键修改
|
||||
*/
|
||||
export function UpHotkey(id: number, key: number, modifier: number): $CancellablePromise<void> {
|
||||
return $Call.ByName("cmbone/internal/services.HotkeyService.UpHotkey", id, key, modifier);
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Hotkey.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
@@ -0,0 +1,19 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as AppConfigService from "./appconfigservice.js";
|
||||
import * as AppService from "./appservice.js";
|
||||
import * as HotkeyService from "./hotkeyservice.js";
|
||||
import * as OCRService from "./ocrservice.js";
|
||||
import * as SystemInfo from "./systeminfo.js";
|
||||
export {
|
||||
AppConfigService,
|
||||
AppService,
|
||||
HotkeyService,
|
||||
OCRService,
|
||||
SystemInfo
|
||||
};
|
||||
|
||||
export {
|
||||
SuiStore
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 sql$0 from "../../../database/sql/models.js";
|
||||
|
||||
export class SuiStore {
|
||||
"DB": sql$0.DB | null;
|
||||
|
||||
/** Creates a new SuiStore instance. */
|
||||
constructor($$source: Partial<SuiStore> = {}) {
|
||||
if (!("DB" in $$source)) {
|
||||
this["DB"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SuiStore instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SuiStore {
|
||||
const $$createField0_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("DB" in $$parsedSource) {
|
||||
$$parsedSource["DB"] = $$createField0_0($$parsedSource["DB"]);
|
||||
}
|
||||
return new SuiStore($$parsedSource as Partial<SuiStore>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = sql$0.DB.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
@@ -0,0 +1,8 @@
|
||||
// This file is kept small because wails3 is not available in this environment.
|
||||
// Regenerate bindings with `wails3 generate bindings` when the CLI is installed.
|
||||
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
export function RecognizeImageBase64(base64str: string): $CancellablePromise<string> {
|
||||
return $Call.ByName("cmbone/internal/services.OCRService.RecognizeImageBase64", base64str);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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 { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
|
||||
|
||||
export function GetOS(): $CancellablePromise<string> {
|
||||
return $Call.ByName("cmbone/internal/services.SystemInfo.GetOS");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
DB
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* DB is a database handle representing a pool of zero or more
|
||||
* underlying connections. It's safe for concurrent use by multiple
|
||||
* goroutines.
|
||||
*
|
||||
* The sql package creates and frees connections automatically; it
|
||||
* also maintains a free pool of idle connections. If the database has
|
||||
* a concept of per-connection state, such state can be reliably observed
|
||||
* within a transaction ([Tx]) or connection ([Conn]). Once [DB.Begin] is called, the
|
||||
* returned [Tx] is bound to a single connection. Once [Tx.Commit] or
|
||||
* [Tx.Rollback] is called on the transaction, that transaction's
|
||||
* connection is returned to [DB]'s idle connection pool. The pool size
|
||||
* can be controlled with [DB.SetMaxIdleConns].
|
||||
*/
|
||||
export class DB {
|
||||
|
||||
/** Creates a new DB instance. */
|
||||
constructor($$source: Partial<DB> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DB instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): DB {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new DB($$parsedSource as Partial<DB>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
FS
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* An FS is a read-only collection of files, usually initialized with a //go:embed directive.
|
||||
* When declared without a //go:embed directive, an FS is an empty file system.
|
||||
*
|
||||
* An FS is a read-only value, so it is safe to use from multiple goroutines
|
||||
* simultaneously and also safe to assign values of type FS to each other.
|
||||
*
|
||||
* FS implements fs.FS, so it can be used with any package that understands
|
||||
* file system interfaces, including net/http, text/template, and html/template.
|
||||
*
|
||||
* See the package documentation for more details about initializing an FS.
|
||||
*/
|
||||
export class FS {
|
||||
|
||||
/** Creates a new FS instance. */
|
||||
constructor($$source: Partial<FS> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new FS instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): FS {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new FS($$parsedSource as Partial<FS>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
App,
|
||||
BrowserManager,
|
||||
ClipboardManager,
|
||||
ContextMenuManager,
|
||||
DialogManager,
|
||||
EnvironmentManager,
|
||||
EventManager,
|
||||
KeyBindingManager,
|
||||
MenuManager,
|
||||
ScreenManager,
|
||||
SystemTrayManager,
|
||||
WindowManager
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,369 @@
|
||||
// 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 slog$0 from "../../../../../../log/slog/models.js";
|
||||
|
||||
export class App {
|
||||
/**
|
||||
* Manager pattern for organized API
|
||||
*/
|
||||
"Window": WindowManager | null;
|
||||
"ContextMenu": ContextMenuManager | null;
|
||||
"KeyBinding": KeyBindingManager | null;
|
||||
"Browser": BrowserManager | null;
|
||||
"Env": EnvironmentManager | null;
|
||||
"Dialog": DialogManager | null;
|
||||
"Event": EventManager | null;
|
||||
"Menu": MenuManager | null;
|
||||
"Screen": ScreenManager | null;
|
||||
"Clipboard": ClipboardManager | null;
|
||||
"SystemTray": SystemTrayManager | null;
|
||||
"Logger": slog$0.Logger | null;
|
||||
|
||||
/** Creates a new App instance. */
|
||||
constructor($$source: Partial<App> = {}) {
|
||||
if (!("Window" in $$source)) {
|
||||
this["Window"] = null;
|
||||
}
|
||||
if (!("ContextMenu" in $$source)) {
|
||||
this["ContextMenu"] = null;
|
||||
}
|
||||
if (!("KeyBinding" in $$source)) {
|
||||
this["KeyBinding"] = null;
|
||||
}
|
||||
if (!("Browser" in $$source)) {
|
||||
this["Browser"] = null;
|
||||
}
|
||||
if (!("Env" in $$source)) {
|
||||
this["Env"] = null;
|
||||
}
|
||||
if (!("Dialog" in $$source)) {
|
||||
this["Dialog"] = null;
|
||||
}
|
||||
if (!("Event" in $$source)) {
|
||||
this["Event"] = null;
|
||||
}
|
||||
if (!("Menu" in $$source)) {
|
||||
this["Menu"] = null;
|
||||
}
|
||||
if (!("Screen" in $$source)) {
|
||||
this["Screen"] = null;
|
||||
}
|
||||
if (!("Clipboard" in $$source)) {
|
||||
this["Clipboard"] = null;
|
||||
}
|
||||
if (!("SystemTray" in $$source)) {
|
||||
this["SystemTray"] = null;
|
||||
}
|
||||
if (!("Logger" in $$source)) {
|
||||
this["Logger"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new App instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): App {
|
||||
const $$createField0_0 = $$createType1;
|
||||
const $$createField1_0 = $$createType3;
|
||||
const $$createField2_0 = $$createType5;
|
||||
const $$createField3_0 = $$createType7;
|
||||
const $$createField4_0 = $$createType9;
|
||||
const $$createField5_0 = $$createType11;
|
||||
const $$createField6_0 = $$createType13;
|
||||
const $$createField7_0 = $$createType15;
|
||||
const $$createField8_0 = $$createType17;
|
||||
const $$createField9_0 = $$createType19;
|
||||
const $$createField10_0 = $$createType21;
|
||||
const $$createField11_0 = $$createType23;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("Window" in $$parsedSource) {
|
||||
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||
}
|
||||
if ("ContextMenu" in $$parsedSource) {
|
||||
$$parsedSource["ContextMenu"] = $$createField1_0($$parsedSource["ContextMenu"]);
|
||||
}
|
||||
if ("KeyBinding" in $$parsedSource) {
|
||||
$$parsedSource["KeyBinding"] = $$createField2_0($$parsedSource["KeyBinding"]);
|
||||
}
|
||||
if ("Browser" in $$parsedSource) {
|
||||
$$parsedSource["Browser"] = $$createField3_0($$parsedSource["Browser"]);
|
||||
}
|
||||
if ("Env" in $$parsedSource) {
|
||||
$$parsedSource["Env"] = $$createField4_0($$parsedSource["Env"]);
|
||||
}
|
||||
if ("Dialog" in $$parsedSource) {
|
||||
$$parsedSource["Dialog"] = $$createField5_0($$parsedSource["Dialog"]);
|
||||
}
|
||||
if ("Event" in $$parsedSource) {
|
||||
$$parsedSource["Event"] = $$createField6_0($$parsedSource["Event"]);
|
||||
}
|
||||
if ("Menu" in $$parsedSource) {
|
||||
$$parsedSource["Menu"] = $$createField7_0($$parsedSource["Menu"]);
|
||||
}
|
||||
if ("Screen" in $$parsedSource) {
|
||||
$$parsedSource["Screen"] = $$createField8_0($$parsedSource["Screen"]);
|
||||
}
|
||||
if ("Clipboard" in $$parsedSource) {
|
||||
$$parsedSource["Clipboard"] = $$createField9_0($$parsedSource["Clipboard"]);
|
||||
}
|
||||
if ("SystemTray" in $$parsedSource) {
|
||||
$$parsedSource["SystemTray"] = $$createField10_0($$parsedSource["SystemTray"]);
|
||||
}
|
||||
if ("Logger" in $$parsedSource) {
|
||||
$$parsedSource["Logger"] = $$createField11_0($$parsedSource["Logger"]);
|
||||
}
|
||||
return new App($$parsedSource as Partial<App>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BrowserManager manages browser-related operations
|
||||
*/
|
||||
export class BrowserManager {
|
||||
|
||||
/** Creates a new BrowserManager instance. */
|
||||
constructor($$source: Partial<BrowserManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new BrowserManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): BrowserManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new BrowserManager($$parsedSource as Partial<BrowserManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ClipboardManager manages clipboard operations
|
||||
*/
|
||||
export class ClipboardManager {
|
||||
|
||||
/** Creates a new ClipboardManager instance. */
|
||||
constructor($$source: Partial<ClipboardManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ClipboardManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): ClipboardManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new ClipboardManager($$parsedSource as Partial<ClipboardManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ContextMenuManager manages all context menu operations
|
||||
*/
|
||||
export class ContextMenuManager {
|
||||
|
||||
/** Creates a new ContextMenuManager instance. */
|
||||
constructor($$source: Partial<ContextMenuManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ContextMenuManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): ContextMenuManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new ContextMenuManager($$parsedSource as Partial<ContextMenuManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DialogManager manages dialog-related operations
|
||||
*/
|
||||
export class DialogManager {
|
||||
|
||||
/** Creates a new DialogManager instance. */
|
||||
constructor($$source: Partial<DialogManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DialogManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): DialogManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new DialogManager($$parsedSource as Partial<DialogManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EnvironmentManager manages environment-related operations
|
||||
*/
|
||||
export class EnvironmentManager {
|
||||
|
||||
/** Creates a new EnvironmentManager instance. */
|
||||
constructor($$source: Partial<EnvironmentManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new EnvironmentManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): EnvironmentManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new EnvironmentManager($$parsedSource as Partial<EnvironmentManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EventManager manages event-related operations
|
||||
*/
|
||||
export class EventManager {
|
||||
|
||||
/** Creates a new EventManager instance. */
|
||||
constructor($$source: Partial<EventManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new EventManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): EventManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new EventManager($$parsedSource as Partial<EventManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyBindingManager manages all key binding operations
|
||||
*/
|
||||
export class KeyBindingManager {
|
||||
|
||||
/** Creates a new KeyBindingManager instance. */
|
||||
constructor($$source: Partial<KeyBindingManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new KeyBindingManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): KeyBindingManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new KeyBindingManager($$parsedSource as Partial<KeyBindingManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MenuManager manages menu-related operations
|
||||
*/
|
||||
export class MenuManager {
|
||||
|
||||
/** Creates a new MenuManager instance. */
|
||||
constructor($$source: Partial<MenuManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MenuManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): MenuManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new MenuManager($$parsedSource as Partial<MenuManager>);
|
||||
}
|
||||
}
|
||||
|
||||
export class ScreenManager {
|
||||
|
||||
/** Creates a new ScreenManager instance. */
|
||||
constructor($$source: Partial<ScreenManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ScreenManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): ScreenManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new ScreenManager($$parsedSource as Partial<ScreenManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemTrayManager manages system tray-related operations
|
||||
*/
|
||||
export class SystemTrayManager {
|
||||
|
||||
/** Creates a new SystemTrayManager instance. */
|
||||
constructor($$source: Partial<SystemTrayManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SystemTrayManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SystemTrayManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new SystemTrayManager($$parsedSource as Partial<SystemTrayManager>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowManager manages all window-related operations
|
||||
*/
|
||||
export class WindowManager {
|
||||
|
||||
/** Creates a new WindowManager instance. */
|
||||
constructor($$source: Partial<WindowManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WindowManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new WindowManager($$parsedSource as Partial<WindowManager>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = WindowManager.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = ContextMenuManager.createFrom;
|
||||
const $$createType3 = $Create.Nullable($$createType2);
|
||||
const $$createType4 = KeyBindingManager.createFrom;
|
||||
const $$createType5 = $Create.Nullable($$createType4);
|
||||
const $$createType6 = BrowserManager.createFrom;
|
||||
const $$createType7 = $Create.Nullable($$createType6);
|
||||
const $$createType8 = EnvironmentManager.createFrom;
|
||||
const $$createType9 = $Create.Nullable($$createType8);
|
||||
const $$createType10 = DialogManager.createFrom;
|
||||
const $$createType11 = $Create.Nullable($$createType10);
|
||||
const $$createType12 = EventManager.createFrom;
|
||||
const $$createType13 = $Create.Nullable($$createType12);
|
||||
const $$createType14 = MenuManager.createFrom;
|
||||
const $$createType15 = $Create.Nullable($$createType14);
|
||||
const $$createType16 = ScreenManager.createFrom;
|
||||
const $$createType17 = $Create.Nullable($$createType16);
|
||||
const $$createType18 = ClipboardManager.createFrom;
|
||||
const $$createType19 = $Create.Nullable($$createType18);
|
||||
const $$createType20 = SystemTrayManager.createFrom;
|
||||
const $$createType21 = $Create.Nullable($$createType20);
|
||||
const $$createType22 = slog$0.Logger.createFrom;
|
||||
const $$createType23 = $Create.Nullable($$createType22);
|
||||
@@ -0,0 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export {
|
||||
Logger
|
||||
} from "./models.js";
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* A Logger records structured information about each call to its
|
||||
* Log, Debug, Info, Warn, and Error methods.
|
||||
* For each call, it creates a [Record] and passes it to a [Handler].
|
||||
*
|
||||
* To create a new Logger, call [New] or a Logger method
|
||||
* that begins "With".
|
||||
*/
|
||||
export class Logger {
|
||||
|
||||
/** Creates a new Logger instance. */
|
||||
constructor($$source: Partial<Logger> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Logger instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Logger {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Logger($$parsedSource as Partial<Logger>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/wails.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>Wails + Vue + TS</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
overflow: hidden; /* 禁止横向滚动 */
|
||||
}
|
||||
html, body, #app {
|
||||
background: transparent !important;
|
||||
}
|
||||
/* 仅当 html 有 dark 类时生效 */
|
||||
|
||||
/* Select 输入框 */
|
||||
html.dark .ant-select-selector {
|
||||
color: #ffffff !important;
|
||||
/* background-color: #1f1f1f !important; */
|
||||
background-color: rgba(31, 41, 55, 1) !important;
|
||||
/* background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1)); */
|
||||
border-color: #434343 !important;
|
||||
}
|
||||
|
||||
/* Select 下拉面板 */
|
||||
html.dark .ant-select-dropdown {
|
||||
/* background-color: #141414 !important; */
|
||||
background-color: rgba(31, 41, 55, 1) !important;
|
||||
}
|
||||
|
||||
/* 下拉项文字 */
|
||||
html.dark .ant-select-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Hover / Active 状态 */
|
||||
html.dark .ant-select-item-option-active {
|
||||
/* background-color: #2a2a2a !important; */
|
||||
background-color: rgb(46, 56, 71) !important;
|
||||
/* background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1)); */
|
||||
}
|
||||
|
||||
/* 选中项 */
|
||||
html.dark .ant-select-item-option-selected {
|
||||
/* background-color: #3a3a3a !important; */
|
||||
background-color: #283150 !important;
|
||||
/* background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1)) !important; */
|
||||
color: #69c0ff !important;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
html.dark .ant-input {
|
||||
background-color: #374151 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
html.dark .ant-input::placeholder {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
|
||||
/* Input 带前后缀 */
|
||||
html.dark .ant-input-affix-wrapper {
|
||||
background-color: #374151 !important;
|
||||
border-color: #626161 !important;
|
||||
}
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:dev": "vue-tsc && vite build --minify false --mode development",
|
||||
"build": "vue-tsc && vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@wailsio/runtime": "3.0.0-alpha.66",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"vue": "^3.2.45",
|
||||
"vue-i18n": "^11.1.5",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^8.1.3",
|
||||
"vue-tsc": "^1.0.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
:root {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("./Inter-Medium.ttf") format("truetype");
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 3em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
color: black;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="797.5" height="834.5" viewBox="0 0 797.5 834.5" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" artist="Katerina Limpitsouni" source="https://undraw.co/"><title>void</title><ellipse cx="308.5" cy="780" rx="308.5" ry="54.5" fill="#3f3d56"/><circle cx="496" cy="301.5" r="301.5" fill="#3f3d56"/><circle cx="496" cy="301.5" r="248.89787" opacity="0.05"/><circle cx="496" cy="301.5" r="203.99362" opacity="0.05"/><circle cx="496" cy="301.5" r="146.25957" opacity="0.05"/><path d="M398.42029,361.23224s-23.70394,66.72221-13.16886,90.42615,27.21564,46.52995,27.21564,46.52995S406.3216,365.62186,398.42029,361.23224Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><path d="M398.42029,361.23224s-23.70394,66.72221-13.16886,90.42615,27.21564,46.52995,27.21564,46.52995S406.3216,365.62186,398.42029,361.23224Z" transform="translate(-201.25 -32.75)" opacity="0.1"/><path d="M415.10084,515.74682s-1.75585,16.68055-2.63377,17.55847.87792,2.63377,0,5.26754-1.75585,6.14547,0,7.02339-9.65716,78.13521-9.65716,78.13521-28.09356,36.8728-16.68055,94.81576l3.51169,58.82089s27.21564,1.75585,27.21564-7.90132c0,0-1.75585-11.413-1.75585-16.68055s4.38962-5.26754,1.75585-7.90131-2.63377-4.38962-2.63377-4.38962,4.38961-3.51169,3.51169-4.38962,7.90131-63.2105,7.90131-63.2105,9.65716-9.65716,9.65716-14.92471v-5.26754s4.38962-11.413,4.38962-12.29093,23.70394-54.43127,23.70394-54.43127l9.65716,38.62864,10.53509,55.3092s5.26754,50.04165,15.80262,69.356c0,0,18.4364,63.21051,18.4364,61.45466s30.72733-6.14547,29.84941-14.04678-18.4364-118.5197-18.4364-118.5197L533.62054,513.991Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><path d="M391.3969,772.97846s-23.70394,46.53-7.90131,48.2858,21.94809,1.75585,28.97148-5.26754c3.83968-3.83968,11.61528-8.99134,17.87566-12.87285a23.117,23.117,0,0,0,10.96893-21.98175c-.463-4.29531-2.06792-7.83444-6.01858-8.16366-10.53508-.87792-22.826-10.53508-22.826-10.53508Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><path d="M522.20753,807.21748s-23.70394,46.53-7.90131,48.28581,21.94809,1.75584,28.97148-5.26754c3.83968-3.83969,11.61528-8.99134,17.87566-12.87285a23.117,23.117,0,0,0,10.96893-21.98175c-.463-4.29531-2.06792-7.83444-6.01857-8.16367-10.53509-.87792-22.826-10.53508-22.826-10.53508Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><circle cx="295.90488" cy="215.43252" r="36.90462" fill="#ffb8b8"/><path d="M473.43048,260.30832S447.07,308.81154,444.9612,308.81154,492.41,324.62781,492.41,324.62781s13.70743-46.39439,15.81626-50.61206Z" transform="translate(-201.25 -32.75)" fill="#ffb8b8"/><path d="M513.86726,313.3854s-52.67543-28.97148-57.943-28.09356-61.45466,50.04166-60.57673,70.2339,7.90131,53.55335,7.90131,53.55335,2.63377,93.05991,7.90131,93.93783-.87792,16.68055.87793,16.68055,122.90931,0,123.78724-2.63377S513.86726,313.3854,513.86726,313.3854Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><path d="M543.2777,521.89228s16.68055,50.91958,2.63377,49.16373-20.19224-43.89619-20.19224-43.89619Z" transform="translate(-201.25 -32.75)" fill="#ffb8b8"/><path d="M498.50359,310.31267s-32.48318,7.02339-27.21563,50.91957,14.9247,87.79237,14.9247,87.79237l32.48318,71.11182,3.51169,13.16886,23.70394-6.14547L528.353,425.32067s-6.14547-108.86253-14.04678-112.37423A33.99966,33.99966,0,0,0,498.50359,310.31267Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><polygon points="277.5 414.958 317.885 486.947 283.86 411.09 277.5 414.958" opacity="0.1"/><path d="M533.896,237.31585l.122-2.82012,5.6101,1.39632a6.26971,6.26971,0,0,0-2.5138-4.61513l5.97581-.33413a64.47667,64.47667,0,0,0-43.1245-26.65136c-12.92583-1.87346-27.31837.83756-36.182,10.43045-4.29926,4.653-7.00067,10.57018-8.92232,16.60685-3.53926,11.11821-4.26038,24.3719,3.11964,33.40938,7.5006,9.18513,20.602,10.98439,32.40592,12.12114,4.15328.4,8.50581.77216,12.35457-.83928a29.721,29.721,0,0,0-1.6539-13.03688,8.68665,8.68665,0,0,1-.87879-4.15246c.5247-3.51164,5.20884-4.39635,8.72762-3.9219s7.74984,1.20031,10.062-1.49432c1.59261-1.85609,1.49867-4.559,1.70967-6.99575C521.28248,239.785,533.83587,238.70653,533.896,237.31585Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><circle cx="559" cy="744.5" r="43" fill="#6c63ff"/><circle cx="54" cy="729.5" r="43" fill="#6c63ff"/><circle cx="54" cy="672.5" r="31" fill="#6c63ff"/><circle cx="54" cy="624.5" r="22" fill="#6c63ff"/></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted} from 'vue'
|
||||
import { themeConfig } from './utils/ThemeManager'
|
||||
const applyTheme = () => {
|
||||
const userTheme = localStorage.getItem('theme')
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const isDark = userTheme === 'dark' || (!userTheme && systemPrefersDark)
|
||||
document.documentElement.classList.toggle('dark', isDark)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
applyTheme()
|
||||
// 可监听系统主题变化自动更新(可选)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
applyTheme()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :theme="themeConfig" >
|
||||
<router-view />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
html, body {
|
||||
margin: 0;
|
||||
overflow-x: hidden; /* 禁止横向滚动 */
|
||||
}
|
||||
/* 可选:全局背景适配 */
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-900 text-black dark:text-white;
|
||||
}
|
||||
.drag-region {
|
||||
-webkit-app-region: drag;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="w-full h-full flex items-center justify-center ">
|
||||
<!-- 中央轻卡片 -->
|
||||
<div
|
||||
class="
|
||||
w-[420px]
|
||||
rounded-2xl
|
||||
bg-neutral-50 dark:bg-gray-800
|
||||
px-8 py-10
|
||||
text-center
|
||||
shadow-sm
|
||||
border border-neutral-100 dark:border-gray-700
|
||||
"
|
||||
>
|
||||
<!-- 轻符号 / 视觉锚点 -->
|
||||
<transition name="fade-up">
|
||||
<div
|
||||
v-if="show"
|
||||
class="flex justify-center mb-4"
|
||||
>
|
||||
<img src="/system/about.svg" class="w-28 h-28" />
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 标题 -->
|
||||
<transition name="fade-up">
|
||||
<h1
|
||||
v-if="show"
|
||||
class="text-2xl font-semibold text-neutral-800 mb-1 dark:text-white"
|
||||
style="transition-delay: 80ms"
|
||||
>
|
||||
cmbone
|
||||
</h1>
|
||||
</transition>
|
||||
<!-- 副标题 -->
|
||||
<transition name="fade-up">
|
||||
<p
|
||||
v-if="show"
|
||||
class="text-sm text-neutral-500 mb-6 dark:text-neutral-300"
|
||||
style="transition-delay: 120ms"
|
||||
>
|
||||
Wails3 · Desktop
|
||||
</p>
|
||||
</transition>
|
||||
<!-- 主文案 -->
|
||||
<transition name="fade-up">
|
||||
<div
|
||||
v-if="show"
|
||||
class="text-sm text-neutral-700 leading-relaxed space-y-3 mb-6 dark:text-white"
|
||||
style="transition-delay: 180ms"
|
||||
>
|
||||
<p></p>
|
||||
<p></p>
|
||||
<div class="h-2"></div>
|
||||
<p>{{ $t('components.about.description_p1') }}</p>
|
||||
<p>{{ $t('components.about.description_p2') }}</p>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 分割线 -->
|
||||
<transition name="fade-up">
|
||||
<div
|
||||
v-if="show"
|
||||
class="flex justify-center mb-6"
|
||||
style="transition-delay: 240ms"
|
||||
>
|
||||
<span class="w-12 h-px bg-neutral-200"></span>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 底部信息 -->
|
||||
<transition name="fade-up">
|
||||
<div
|
||||
v-if="show"
|
||||
class="text-xs text-neutral-400 space-y-1 dark:text-white"
|
||||
style="transition-delay: 300ms"
|
||||
>
|
||||
<p>Designed & Built by {{ $t('components.about.author') }}</p>
|
||||
<p>© 2026 cmbone</p>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
const show = ref(false)
|
||||
onMounted(() => {
|
||||
// 延迟出现,避免“页面一闪全出”
|
||||
setTimeout(() => {
|
||||
show.value = true
|
||||
}, 80)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-up-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.fade-up-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.fade-up-enter-active {
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class=" text-slate-800 font-sans dark:text-white">
|
||||
<div class="max-w-4xl mx-auto pl-6 pr-6">
|
||||
<header class="flex items-center justify-between mb-6">
|
||||
<div class="relative w-2/3">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search reminders..."
|
||||
class="w-full dark:bg-gray-800 pl-10 pr-4 py-2 bg-slate-100 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all outline-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button @click="showMsgModal" class="relative text-slate-600 w-[40px]">
|
||||
<div class="absolute mr-2 top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white dark:border-gray-200"></div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 dark:text-gray-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="OpenSecondWindow" class="w-[110px] bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-3 py-2 rounded-xl flex items-center gap-2 font-medium transition-colors shadow-sm">
|
||||
<span class="text-xl pb-1">+</span> {{ $t('components.dashboard.buttons.newtask') }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="mb-4 dark:text-white ">
|
||||
<span class="text-xl font-bold text-slate-900 text-left dark:text-white">{{ greetingMessage }}, {{ profile.name }}</span>
|
||||
<p class="text-slate-500 mt-1 text-left dark:text-slate-300" v-if="today_pending>0">You have <span class="font-semibold text-slate-600 dark:text-slate-100" v-text="today_pending"></span> tasks to complete today.</p>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div v-for="stat in stats" :key="stat.label" class="dark:bg-gray-800 dark:text-white bg-white pt-2 pr-4 pl-4 rounded-3xl shadow-sm 0">
|
||||
<div class="flex justify-between mb-2">
|
||||
<div :class="`p-1 rounded-xl ${stat.iconBg} `">
|
||||
<component :is="stat.icon" class="w-6 h-6 flex items-center justify-center " :class="stat.iconColor" />
|
||||
</div>
|
||||
<span :class="tagStyles(stat.tagType)">{{ stat.tag }}</span>
|
||||
</div>
|
||||
<p class="text-slate-500 text-sm font-medium text-left dark:text-white">{{ stat.label }}</p>
|
||||
<h2 class="text-2xl font-bold mt-1 text-slate-800 dark:text-white">{{ stat.value }}</h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4 flex items-center justify-between">
|
||||
<span class="text-base font-bold text-slate-800 dark:text-white">{{ $t('components.dashboard.title.upcoming_task') }}</span>
|
||||
<div class="flex bg-slate-200/60 p-1 rounded-lg text-sm dark:bg-gray-800">
|
||||
<button class="px-4 py-1 rounded-md bg-white shadow-sm font-medium dark:text-white dark:bg-gray-900">{{ $t('components.dashboard.buttons.upcoming_all') }}</button>
|
||||
<button class="px-4 py-1 w-[66px] rounded-md text-slate-500 hover:text-slate-700 dark:text-slate-300">{{ $t('components.dashboard.buttons.upcoming_priority') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="space-y-1 ">
|
||||
<div v-if="tasks.length === 0" class="text-center py-10">
|
||||
<img src="/system/empty.svg" class=" mx-auto mt-1 w-32 " />
|
||||
<span class="text-sm">{{ $t('components.dashboard.label.no_tasks') }}</span>
|
||||
</div>
|
||||
<div v-else v-for="task in tasks" :key="task.id"
|
||||
class="dark:bg-gray-800 dark:text-white dark:border-slate-500 group flex items-center bg-white p-2 rounded-3xl border border-slate-100 hover:shadow-md transition-shadow cursor-pointer">
|
||||
<div class="w-6 h-6 rounded-full border-2 border-slate-200 mr-4 flex items-center justify-center group-hover:border-indigo-400 transition-colors">
|
||||
<div v-if="task.isCompleted" class="w-4 h-4 bg-indigo-500 rounded-full"></div>
|
||||
</div>
|
||||
<div class="flex-1 pl-3">
|
||||
<span class="text-slate-800 text-left text-sm dark:text-white line-clamp-1">{{ task.title }}</span>
|
||||
<div class="flex items-center gap-4 ">
|
||||
<span class="flex items-center text-xs text-slate-400 font-medium">
|
||||
<svg v-if="task.time.includes(':')" xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
|
||||
{{task.time}}
|
||||
</span>
|
||||
|
||||
<span v-if="task.priority" class="flex items-center text-xs text-orange-500 font-bold uppercase tracking-wider">
|
||||
<span class="mr-1">🚩</span> {{ task.priority }}
|
||||
</span>
|
||||
|
||||
<span :class="categoryStyles(task.category)">
|
||||
{{ task.category }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-modal v-model:open="openmessage" title="" width="400px" :closable="false" :footer="null">
|
||||
<div class="bg-white h-[360px] pl-1 pr-1 pb-4 pt-1 ">
|
||||
<Notifications :handle-cancel="openmessage" @notification-change="handleOk" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref ,computed} from 'vue'
|
||||
import { ProfileOutlined,FileDoneOutlined ,ExceptionOutlined } from '@ant-design/icons-vue'
|
||||
import Notifications from '../Setting/Notifications.vue'
|
||||
import { OpenSecondWindow } from '../../../bindings/cmbone/internal/services/appservice'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const today_pending = ref(0)
|
||||
const profile = ref({
|
||||
name: 'Jin Gong',
|
||||
avatar: 'https://i.pravatar.cc/150?img=3'
|
||||
})
|
||||
const openmessage = ref<boolean>(false);
|
||||
const stats = ref([
|
||||
{ label: t('components.dashboard.label.total_tasks'), value: 520, tag: '+12%', tagType: 'percentage', icon: ProfileOutlined, iconBg: 'bg-indigo-100', iconColor: 'text-indigo-600' },
|
||||
{ label: t('components.dashboard.label.pending_tasks'), value: 13, tag: 'Urgent', tagType: 'status', icon: ExceptionOutlined, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' },
|
||||
{ label: t('components.dashboard.label.completed_tasks'), value: 14, tag: 'Daily', tagType: 'default', icon: FileDoneOutlined, iconBg: 'bg-emerald-100', iconColor: 'text-emerald-600' },
|
||||
])
|
||||
interface Task {
|
||||
id: number
|
||||
title: string
|
||||
time: string
|
||||
category: string
|
||||
priority?: string
|
||||
isCompleted: boolean
|
||||
}
|
||||
const tasks = ref<Task[]>([])
|
||||
tasks.value = [
|
||||
// { id: 1, title: 'Quarterly Budget Review', time: '10:30 AM', category: 'WORK', priority: 'High Priority', isCompleted: true },
|
||||
// { id: 2, title: 'Buy Weekly Groceries', time: '05:00 PM', category: 'PERSONAL', isCompleted: false },
|
||||
// { id: 3, title: 'Client Presentation Design', time: 'Tomorrow', category: 'WORK', isCompleted: false },
|
||||
// { id: 4, title: 'Book Gym Slot', time: '08:00 AM', category: 'PERSONAL', isCompleted: false },
|
||||
]
|
||||
const showMsgModal = () => {
|
||||
openmessage.value = true;
|
||||
};
|
||||
const handleOk = () => {
|
||||
openmessage.value = false;
|
||||
};
|
||||
const tagStyles = (type: string) => {
|
||||
const base = "text-[10px] px-2 my-1 rounded-lg font-bold "
|
||||
if (type === 'percentage') return base + "bg-emerald-50 text-emerald-500"
|
||||
if (type === 'status') return base + "bg-orange-50 text-orange-500"
|
||||
return base + "bg-slate-100 text-slate-400"
|
||||
}
|
||||
|
||||
const categoryStyles = (cat: string) => {
|
||||
const base = "text-[8px] px-2 rounded-md font-bold "
|
||||
return cat === 'WORK'
|
||||
? base + "bg-blue-100 text-blue-500"
|
||||
: base + "bg-emerald-100 text-emerald-500"
|
||||
}
|
||||
onMounted(async () => {
|
||||
})
|
||||
|
||||
const now = ref(new Date())
|
||||
|
||||
// 计算问候语
|
||||
const greetingMessage = computed(() => {
|
||||
const hour = now.value.getHours()
|
||||
if (hour >= 5 && hour < 12) return 'Good Morning'
|
||||
if (hour >= 12 && hour < 18) return 'Good Afternoon'
|
||||
if (hour >= 18 && hour < 22) return 'Good Evening'
|
||||
return 'Good Night'
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以在此添加特定字体,如 Inter 或 Plus Jakarta Sans */
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref,h } from 'vue'
|
||||
import ListItem from '../Setting/ListRow.vue'
|
||||
import LanguageSwitcher from '../Setting/LanguageSwitcher.vue'
|
||||
import ThemeSetting from '../Setting/ThemeSetting.vue'
|
||||
import Shortcut from '../Shortcut/Index.vue'
|
||||
|
||||
const modelValue = ref(false)
|
||||
</script>
|
||||
|
||||
<template >
|
||||
<div >
|
||||
<section >
|
||||
<!-- 外观设置 -->
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-white" >{{ $t('components.general.title.exterior') }}</h2>
|
||||
<div class="bg-white rounded-lg shadow divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
|
||||
<ListItem :label="$t('components.general.label.language')" subLabel="">
|
||||
<LanguageSwitcher />
|
||||
</ListItem>
|
||||
<ListItem :label="$t('components.general.label.theme')" subLabel="">
|
||||
<ThemeSetting />
|
||||
</ListItem>
|
||||
</div>
|
||||
<!-- 快捷键设置 -->
|
||||
<Shortcut />
|
||||
|
||||
<!-- 应用设置 -->
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-white" >{{ $t('components.general.title.application') }}</h2>
|
||||
<div class="bg-white rounded-lg shadow divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
|
||||
<ListItem :label="$t('components.general.label.startup')" subLabel="">
|
||||
<input type="checkbox" class="sr-only peer" v-model="modelValue" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer dark:bg-gray-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500 relative"
|
||||
></div>
|
||||
</ListItem>
|
||||
</div>
|
||||
<!-- 应用更新 -->
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-white" >{{ $t('components.general.title.update') }}</h2>
|
||||
<div class="mt-4 bg-white rounded-lg shadow divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
|
||||
<ListItem :label="$t('components.general.label.automatic_up')" subLabel="">
|
||||
<input type="checkbox" class="sr-only peer" v-model="modelValue" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer dark:bg-gray-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500 relative"
|
||||
></div>
|
||||
</ListItem>
|
||||
<ListItem :label="$t('components.general.label.next_up')" subLabel="">
|
||||
<input type="checkbox" class="sr-only peer" v-model="modelValue" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer dark:bg-gray-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-500 relative"
|
||||
></div>
|
||||
</ListItem>
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
h2{
|
||||
text-align: left;
|
||||
margin:15px 15px 4px 15px;
|
||||
font-size:15px;
|
||||
}
|
||||
.rg_desc{
|
||||
text-align: left;
|
||||
margin:4px 0px 4px 8px;
|
||||
font-size:13px;
|
||||
color:#999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="flex h-screen font-sans text-slate-900">
|
||||
<!-- 左侧请求栏 -->
|
||||
<aside
|
||||
class="w-44 border-r border-slate-200 bg-white flex flex-col dark:bg-gray-800 dark:text-white dark:border-slate-600"
|
||||
:class="ismacos ? 'pt-10 ' : 'pt-8'">
|
||||
<nav class="flex-1 px-4 space-y-1 dark:bg-gray-800">
|
||||
<div v-for="(item, index) in requests" :key="index" @click="handleMenu(item)"
|
||||
:class="[' flex items-center gap-3 px-3 py-2 text-indigo-700 cursor-pointer rounded-xl font-medium dark:text-white', selected === item.id ? 'bg-indigo-50 dark:bg-slate-500' : ' hover:bg-slate-50 text-slate-500 dark:hover:bg-slate-700']">
|
||||
<component :is="item.icon" />
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</nav>
|
||||
<div class="pl-4 pr-4 pt-2 pb-2 border-t border-slate-100 flex items-center gap-3 dark:border-slate-600 ">
|
||||
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=JinGong8"
|
||||
class="w-10 h-10 rounded-full bg-slate-200" />
|
||||
<div class="text-sm">
|
||||
<p class="font-bold mb-0">Jin Gong</p>
|
||||
<p class="text-slate-500 text-xs mb-0">Pro Plan</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- 右侧整体 -->
|
||||
<div class="flex-1 flex flex-col bg-gray-50 dark:bg-gray-950">
|
||||
<div v-if="iswindows" class="titlebar drag-region pt-4 px-4 mb-2 text-white flex items-center justify-between">
|
||||
<div class="drag-area">
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button @click="Window.Minimise()" >
|
||||
<LineOutlined class="text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-gray-100" />
|
||||
</button>
|
||||
<button @click="toggleMaximize()">
|
||||
<BorderOutlined v-if="!isMaximized" class="text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-gray-100" />
|
||||
<SwitcherOutlined v-else class="text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-gray-100" />
|
||||
</button>
|
||||
<button @click="Window.Hide()" >
|
||||
<CloseOutlined class="text-gray-400 hover:text-gray-600 dark:text-gray-300 dark:hover:text-gray-100" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主内容 -->
|
||||
<main class="flex-1 overflow-y-auto max-h-screen scroll-container scrollbar-thin" :class="iswindows ? 'px-4 pt-px pb-4' : 'p-4'">
|
||||
<component :is="getComponent" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SettingOutlined, BulbOutlined, ScheduleOutlined, DashboardOutlined, TagsOutlined } from '@ant-design/icons-vue';
|
||||
import { OpenSecondWindow } from '../../../bindings/cmbone/internal/services/appservice'
|
||||
import { getOS, OS_READY } from '../../utils/osinfo'
|
||||
import { CloseOutlined, LineOutlined, BorderOutlined, SwitcherOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const ismacos = ref(false)
|
||||
const iswindows= ref(false)
|
||||
const { t } = useI18n()
|
||||
const isMaximized = ref(false)
|
||||
//菜单结构
|
||||
type MenuItem =
|
||||
| {
|
||||
id: string
|
||||
label: string
|
||||
icon: any
|
||||
type: 'component'
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
label: string
|
||||
icon: any
|
||||
type: 'window'
|
||||
action: () => void
|
||||
}
|
||||
|
||||
const requests = computed<MenuItem[]>(() => [
|
||||
{ id: 'dashboard', label: t('menus.dashboard'), icon: DashboardOutlined, type: 'component' },
|
||||
{ id: 'light-tip', label: t('menus.second'), icon: ScheduleOutlined, type: 'window', action: OpenSecondWindow },
|
||||
{id:'tags',label:t('menus.tags'),icon:TagsOutlined,type: 'component'},
|
||||
{ id: 'setting', label: t('menus.setting'), icon: SettingOutlined, type: 'component' },
|
||||
{ id: 'about', label: t('menus.about'), icon: BulbOutlined, type: 'component' },
|
||||
])
|
||||
const selected = ref('dashboard')
|
||||
import Dashboard from '../Dashboard/Index.vue'
|
||||
import Tags from '../Tags/Index.vue'
|
||||
import Setting from '../General/Index.vue'
|
||||
import About from '../About/Index.vue'
|
||||
import { Window } from '@wailsio/runtime';
|
||||
|
||||
|
||||
const components = {
|
||||
dashboard: Dashboard,
|
||||
tags: Tags,
|
||||
setting: Setting,
|
||||
about: About
|
||||
}
|
||||
|
||||
const getComponent = computed(() => components[selected.value])
|
||||
function handleMenu(item: MenuItem) {
|
||||
if (item.type === 'component') {
|
||||
selected.value = item.id
|
||||
}
|
||||
if (item.type === 'window') {
|
||||
item.action()
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单变化时滚动到顶部
|
||||
watch(selected, () => {
|
||||
nextTick(() => {
|
||||
const el = document.querySelector('.scroll-container')
|
||||
if (el) el.scrollTop = 0
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await OS_READY
|
||||
const osname = getOS() //判断是否为macos
|
||||
if (osname === 'darwin') {
|
||||
ismacos.value = true;
|
||||
}else if (osname === 'windows') {
|
||||
iswindows.value = true;
|
||||
isMaximized.value = await Window.IsMaximised()
|
||||
}else{
|
||||
}
|
||||
})
|
||||
async function toggleMaximize() {
|
||||
isMaximized.value = !isMaximized.value
|
||||
await Window.ToggleMaximise()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
/* 禁止横向滚动 */
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.drag-region {
|
||||
-webkit-app-region: drag;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drag-area {
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
/* 👇 关键:允许拖动窗口 */
|
||||
-webkit-app-region: drag;
|
||||
--wails-draggable: drag;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
/* 👇 按钮不能拖动 */
|
||||
-webkit-app-region: no-drag;
|
||||
--wails-draggable: no-drag;
|
||||
}
|
||||
|
||||
.window-controls button {
|
||||
margin-right: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
/* 全局样式(可放在 main.css 或 tailwind.css) */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div >
|
||||
<a-select class="dark:text-white"
|
||||
ref="select"
|
||||
v-model:value="locale"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-select-option value="zh" >简体中文</a-select-option>
|
||||
<a-select-option value="en">English</a-select-option>
|
||||
<a-select-option value="zh-HK">繁體中文</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { i18n, setupI18n } from '../../utils/i18n'
|
||||
import { Locale } from '../../locales'
|
||||
import { SetLanguage } from '../../../bindings/cmbone/internal/services/appservice'
|
||||
const locale = computed({
|
||||
get: () => i18n.global.locale.value,
|
||||
set: (val) => {
|
||||
switchLang(val as Locale)
|
||||
}
|
||||
})
|
||||
const switchLang =async (lang: Locale) => {
|
||||
await setupI18n(lang) // 切换语言并更新 i18n 实例
|
||||
await SetLanguage(lang)// 更新配置项中的语言设置 and 更新菜单语言
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
subLabel?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-4 py-3 ">
|
||||
<div style="text-align: left;">
|
||||
<div class="text-sm text-gray-900 font-medium dark:text-white">{{ label }}</div>
|
||||
<div v-if="subLabel" class="text-xs text-gray-500 mt-1">{{ subLabel }}</div>
|
||||
</div>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue';
|
||||
import { CloseOutlined, DeleteOutlined, SyncOutlined } from '@ant-design/icons-vue'
|
||||
const props = defineProps<{
|
||||
handleCancel: boolean,
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'notification-change', value: true | false): void
|
||||
}>()
|
||||
function onCloseChange() {
|
||||
emit('notification-change', false)
|
||||
}
|
||||
interface Activity {
|
||||
id: number
|
||||
title: string
|
||||
content?: string
|
||||
type: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
const activities_data = ref<Activity[]>([
|
||||
{
|
||||
id: 1,
|
||||
title: 'System Update',
|
||||
content: 'Your system was updated successfully.',
|
||||
type: 'system',
|
||||
created_at: Math.floor(Date.now() / 1000) - 300
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Auto-complete Task',
|
||||
content: 'Task "Write report" was auto-completed.',
|
||||
type: 'auto_complete',
|
||||
created_at: Math.floor(Date.now() / 1000) - 7200
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Alert: High CPU Usage',
|
||||
content: 'Your CPU usage has been above 90% for the last hour.',
|
||||
type: 'alert',
|
||||
created_at: Math.floor(Date.now() / 1000) - 90000
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'New Message',
|
||||
content: 'You have received a new message from John.',
|
||||
type: 'message',
|
||||
created_at: Math.floor(Date.now() / 1000) - 120
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Backup Completed',
|
||||
content: 'Your files were backed up successfully.',
|
||||
type: 'backup',
|
||||
created_at: Math.floor(Date.now() / 1000) - 3600
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Friend Request',
|
||||
content: 'Alice has sent you a friend request.',
|
||||
type: 'social',
|
||||
created_at: Math.floor(Date.now() / 1000) - 18000
|
||||
}
|
||||
])
|
||||
const activities = ref<Activity[]>([])
|
||||
// 时间格式
|
||||
function formatTime(ts: number) {
|
||||
const now = Date.now() / 1000
|
||||
const diff = now - ts
|
||||
if (diff < 60) return 'just now'
|
||||
if (diff < 3600) return Math.floor(diff / 60) + ' mins ago'
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + ' hours ago'
|
||||
return 'yesterday'
|
||||
}
|
||||
|
||||
// 类型样式
|
||||
function getTypeStyle(type: string) {
|
||||
switch (type) {
|
||||
case 'system':
|
||||
return {
|
||||
icon: 'i-carbon-information',
|
||||
bg: 'bg-indigo-100',
|
||||
iconColor: 'text-indigo-600'
|
||||
}
|
||||
case 'auto_complete':
|
||||
return {
|
||||
icon: 'i-carbon-checkmark-filled',
|
||||
bg: 'bg-green-100',
|
||||
iconColor: 'text-green-600'
|
||||
}
|
||||
case 'complete':
|
||||
return {
|
||||
icon: 'i-carbon-checkmark-filled',
|
||||
bg: 'bg-green-200',
|
||||
iconColor: 'text-green-800'
|
||||
}
|
||||
case 'alert':
|
||||
return {
|
||||
icon: 'i-carbon-warning-filled',
|
||||
bg: 'bg-orange-100',
|
||||
iconColor: 'text-orange-600'
|
||||
}
|
||||
case 'create':
|
||||
return {
|
||||
icon: 'i-carbon-add-filled',
|
||||
bg: 'bg-blue-100',
|
||||
iconColor: 'text-blue-600'
|
||||
}
|
||||
default:
|
||||
return {
|
||||
icon: 'i-carbon-dot-mark',
|
||||
bg: 'bg-gray-100',
|
||||
iconColor: 'text-gray-500'
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
await onloadActivities()
|
||||
})
|
||||
const onloadActivities = async () => {
|
||||
try {
|
||||
activities.value = activities_data.value
|
||||
} catch (error) {
|
||||
console.error('Failed to load activities:', error)
|
||||
}
|
||||
}
|
||||
const clearActivities = async () => {
|
||||
try {
|
||||
activities.value = []
|
||||
} catch (error) {
|
||||
console.error('Failed to clear activities:', error)
|
||||
}
|
||||
}
|
||||
const clearActivity = async (id: number) => {
|
||||
try {
|
||||
activities.value = activities.value.filter(item => item.id !== id)
|
||||
message.success('Deleted successfully.')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear activity:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between mb-4 mt-[-4px]">
|
||||
<div class="text-sm font-semibold flex items-center gap-2 text-slate-800 dark:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 dark:text-gray-200" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
Notifications
|
||||
</div>
|
||||
<div class=" cursor-pointer text-items-center text-sm flex gap-4">
|
||||
<DeleteOutlined @click="clearActivities" class="text-red-400 hover:text-red-600" />
|
||||
<SyncOutlined @click="onloadActivities" class="text-yellow-400 hover:text-yellow-600" />
|
||||
<CloseOutlined @click="onCloseChange" class="text-gray-400 hover:text-gray-600" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white h-[320px] ">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="activities.length === 0" class="text-center text-gray-400 text-sm py-6">
|
||||
<img src="/system/reminders.svg" alt="No activity" class="w-28 h-28 mx-auto mb-2" />
|
||||
No activity yet
|
||||
</div>
|
||||
<div v-else class="space-y-1 overflow-y-auto h-full scrollbar-thin pr-2 ">
|
||||
<div v-for="item in activities" :key="item.id" class="flex gap-2 group">
|
||||
<div class="flex-1 p-1 rounded-xl transition-all duration-200
|
||||
hover:bg-gray-50 ">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-xl flex items-center justify-center" :class="getTypeStyle(item.type).bg">
|
||||
<i :class="[
|
||||
getTypeStyle(item.type).icon,
|
||||
getTypeStyle(item.type).iconColor,
|
||||
'text-lg'
|
||||
]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 sm:w-auto">
|
||||
<div class="flex items-start justify-between gap-2 ">
|
||||
<!-- 标题 -->
|
||||
<div class="text-xs font-medium text-gray-800 break-words line-clamp-1">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<!-- 时间 -->
|
||||
<div class="text-xs text-gray-400 whitespace-nowrap shrink-0 ">
|
||||
{{ formatTime(item.created_at) }}
|
||||
<CloseOutlined @click="clearActivity(item.id)"
|
||||
class="text-red-400 hover:text-red-600 pl-1 group-hover:opacity-100 transition-opacity cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.content" class="text-xs text-gray-500 mt-1">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[60%] items-center justify-center mx-auto ">
|
||||
<a-divider dashed plain>END</a-divider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 全局样式(可放在 main.css 或 tailwind.css) */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-popover {
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps<{ modelValue: string }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const modifierKeys = ['Shift', 'Control', 'Alt', 'Meta'] as const;
|
||||
const modifierSymbols: Record<string, string> = {
|
||||
Shift: '⇧',
|
||||
Control: '⌃',
|
||||
Alt: '⌥',
|
||||
Meta: '⌘',
|
||||
};
|
||||
|
||||
// 用于规范化外部传入的别名,比如 Cmd => Meta
|
||||
const normalizeModifier = (key: string): string => {
|
||||
switch (key.toUpperCase()) {
|
||||
case 'CMD':
|
||||
case 'COMMAND':
|
||||
return 'Meta';
|
||||
case 'CTRL':
|
||||
return 'Control';
|
||||
case 'OPTION':
|
||||
return 'Alt';
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
const pressedModifiers = ref<Set<string>>(new Set());
|
||||
const finalModifiers = ref<Set<string>>(new Set());
|
||||
const mainKey = ref<string | null>(null);
|
||||
|
||||
const isRecording = ref(false);
|
||||
const isFinalized = ref(false);
|
||||
|
||||
const display = computed(() => {
|
||||
const activeSet = isFinalized.value ? finalModifiers.value : pressedModifiers.value;
|
||||
|
||||
const modifiers = modifierKeys
|
||||
.map(key => {
|
||||
const isActive = activeSet.has(key);
|
||||
const color = isActive ? '#1890ff' : '#999';
|
||||
return `<span style=\"color: ${color}; font-weight: bold;\">${modifierSymbols[key]}</span>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const keyColor = isFinalized.value && mainKey.value ? '#1890ff' : '#000';
|
||||
const keyPart = mainKey.value
|
||||
? `<span style=\"margin-left: 6px; color: ${keyColor}; font-weight: bold;\">${mainKey.value.toUpperCase()}</span>`
|
||||
: '';
|
||||
|
||||
return modifiers + keyPart;
|
||||
});
|
||||
|
||||
const shortcutString = computed(() => {
|
||||
if (!isFinalized.value || !mainKey.value || finalModifiers.value.size === 0) {
|
||||
return '';
|
||||
}
|
||||
return [...finalModifiers.value].join('+') + '+' + mainKey.value.toUpperCase();
|
||||
});
|
||||
|
||||
watch(shortcutString, (val) => {
|
||||
emit('update:modelValue', val);
|
||||
});
|
||||
|
||||
const startRecording = () => {
|
||||
isRecording.value = true;
|
||||
isFinalized.value = false;
|
||||
mainKey.value = null;
|
||||
pressedModifiers.value.clear();
|
||||
finalModifiers.value.clear();
|
||||
};
|
||||
|
||||
const finalize = () => {
|
||||
if (pressedModifiers.value.size === 0 || !mainKey.value) {
|
||||
//message.warning('快捷键无效:必须包含至少一个修饰键和一个主键');
|
||||
startRecording();
|
||||
return;
|
||||
}
|
||||
isFinalized.value = true;
|
||||
finalModifiers.value = new Set(pressedModifiers.value);
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
isRecording.value = false;
|
||||
isFinalized.value = false;
|
||||
pressedModifiers.value.clear();
|
||||
finalModifiers.value.clear();
|
||||
mainKey.value = null;
|
||||
emit('update:modelValue', '');
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isRecording.value || isFinalized.value) return;
|
||||
|
||||
if (modifierKeys.includes(e.key as any)) {
|
||||
pressedModifiers.value.add(e.key);
|
||||
} else if (!['Tab', 'Escape'].includes(e.key)) {
|
||||
mainKey.value = e.key.length === 1 ? e.key.toUpperCase() : e.key;
|
||||
finalize();
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (!isRecording.value || isFinalized.value) return;
|
||||
if (modifierKeys.includes(e.key as any)) {
|
||||
pressedModifiers.value.delete(e.key);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 初始化值支持:从 modelValue 拆解 modifier 和主键
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (!val) {
|
||||
clearAll();
|
||||
return;
|
||||
}
|
||||
|
||||
//const parts = val.split('+');
|
||||
const parts = val.split('+').map(normalizeModifier);
|
||||
const mods = parts.filter(p => modifierKeys.includes(p as any));
|
||||
const key = parts.find(p => !modifierKeys.includes(p as any));
|
||||
|
||||
if (mods.length && key) {
|
||||
finalModifiers.value = new Set(mods);
|
||||
mainKey.value = key;
|
||||
isFinalized.value = true;
|
||||
} else {
|
||||
clearAll(); // 防止非法值
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-input
|
||||
style="width: 100px;"
|
||||
readonly
|
||||
:value="''"
|
||||
@focus="startRecording"
|
||||
@blur="isRecording = false"
|
||||
>
|
||||
<template #suffix>
|
||||
<span v-html="display" />
|
||||
<!-- <template v-if="isFinalized">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
style="margin-left: 8px; color: #999;"
|
||||
@click.stop="clearAll"
|
||||
>✕</a-button>
|
||||
</template> -->
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { setTheme, getCurrentTheme, ThemeMode } from '../../utils/ThemeManager'
|
||||
|
||||
const themevalue = ref<ThemeMode>('light')
|
||||
|
||||
const themeChange = (val: ThemeMode) => {
|
||||
setTheme(val)
|
||||
// 通知子窗口也可以加上 BroadcastChannel
|
||||
const bc = new BroadcastChannel('theme')
|
||||
bc.postMessage(val)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
themevalue.value = getCurrentTheme()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<a-select
|
||||
v-model:value="themevalue"
|
||||
style="width: 100px"
|
||||
@change="themeChange"
|
||||
>
|
||||
<a-select-option value="light">{{$t('components.themesetting.select.opt_light')}}</a-select-option>
|
||||
<a-select-option value="dark">{{$t('components.themesetting.select.opt_dark')}}</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps<{ modelValue: string }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const modifierKeys = ['Shift', 'Control', 'Alt', 'Meta'] as const;
|
||||
|
||||
const modifierLabels: Record<string, string> = {
|
||||
Shift: 'Shift',
|
||||
Control: 'Ctrl',
|
||||
Alt: 'Alt',
|
||||
Meta: 'Win', // Windows键
|
||||
};
|
||||
|
||||
const normalizeModifier = (key: string): string => {
|
||||
switch (key.toUpperCase()) {
|
||||
case 'CMD':
|
||||
case 'COMMAND':
|
||||
case 'WIN':
|
||||
case 'WINDOWS':
|
||||
return 'Meta';
|
||||
case 'CTRL':
|
||||
return 'Control';
|
||||
case 'OPTION':
|
||||
return 'Alt';
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
const pressedModifiers = ref<Set<string>>(new Set());
|
||||
const finalModifiers = ref<Set<string>>(new Set());
|
||||
const mainKey = ref<string | null>(null);
|
||||
|
||||
const isRecording = ref(false);
|
||||
const isFinalized = ref(false);
|
||||
|
||||
const display = computed(() => {
|
||||
const activeSet = isFinalized.value ? finalModifiers.value : pressedModifiers.value;
|
||||
|
||||
const modifiers = modifierKeys
|
||||
.map(key => {
|
||||
const isActive = activeSet.has(key);
|
||||
const color = isActive ? '#1890ff' : '#999';
|
||||
return `<span style="color: ${color}; font-weight: bold;">${modifierLabels[key]}</span>`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const keyColor = isFinalized.value && mainKey.value ? '#1890ff' : '#000';
|
||||
const keyPart = mainKey.value
|
||||
? `<span style="margin-left: 6px; color: ${keyColor}; font-weight: bold;">${mainKey.value.toUpperCase()}</span>`
|
||||
: '';
|
||||
|
||||
return modifiers + (mainKey.value ? ' ' : '') + keyPart;
|
||||
});
|
||||
|
||||
const shortcutString = computed(() => {
|
||||
if (!isFinalized.value || !mainKey.value || finalModifiers.value.size === 0) {
|
||||
return '';
|
||||
}
|
||||
return [...finalModifiers.value].join('+') + '+' + mainKey.value.toUpperCase();
|
||||
});
|
||||
|
||||
watch(shortcutString, (val) => {
|
||||
emit('update:modelValue', val);
|
||||
});
|
||||
|
||||
const startRecording = () => {
|
||||
isRecording.value = true;
|
||||
isFinalized.value = false;
|
||||
mainKey.value = null;
|
||||
pressedModifiers.value.clear();
|
||||
finalModifiers.value.clear();
|
||||
};
|
||||
|
||||
const finalize = () => {
|
||||
if (pressedModifiers.value.size === 0 || !mainKey.value) {
|
||||
startRecording();
|
||||
return;
|
||||
}
|
||||
isFinalized.value = true;
|
||||
finalModifiers.value = new Set(pressedModifiers.value);
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
isRecording.value = false;
|
||||
isFinalized.value = false;
|
||||
pressedModifiers.value.clear();
|
||||
finalModifiers.value.clear();
|
||||
mainKey.value = null;
|
||||
emit('update:modelValue', '');
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isRecording.value || isFinalized.value) return;
|
||||
|
||||
if (modifierKeys.includes(e.key as any)) {
|
||||
pressedModifiers.value.add(e.key);
|
||||
} else if (!['Tab', 'Escape'].includes(e.key)) {
|
||||
mainKey.value = e.key.length === 1 ? e.key.toUpperCase() : e.key;
|
||||
finalize();
|
||||
|
||||
// 主键按下后立即取消焦点(触发 blur 逻辑)
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
if (!isRecording.value || isFinalized.value) return;
|
||||
if (modifierKeys.includes(e.key as any)) {
|
||||
pressedModifiers.value.delete(e.key);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
});
|
||||
|
||||
// 初始化值支持:从 modelValue 拆解 modifier 和主键
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (!val) {
|
||||
clearAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = val.split('+').map(normalizeModifier);
|
||||
const mods = parts.filter(p => modifierKeys.includes(p as any));
|
||||
const key = parts.find(p => !modifierKeys.includes(p as any));
|
||||
|
||||
if (mods.length && key) {
|
||||
finalModifiers.value = new Set(mods);
|
||||
mainKey.value = key;
|
||||
isFinalized.value = true;
|
||||
} else {
|
||||
clearAll(); // 防止非法值
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-input
|
||||
style="width: 160px;"
|
||||
readonly
|
||||
:value="''"
|
||||
@focus="startRecording"
|
||||
@mousedown.stop
|
||||
@blur="isRecording = false"
|
||||
>
|
||||
<template #suffix>
|
||||
<span v-html="display" />
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { ref,watch ,onMounted,nextTick} from 'vue'
|
||||
import ListItem from '../Setting/ListRow.vue'
|
||||
import ShortcutInput from '../Setting/ShortcutInput.vue';
|
||||
import WinShortcutInput from '../Setting/WinShortcutInput.vue';
|
||||
import { parseShortcutToHotkeyWin,parseShortcutToHotkey,formatHotkeyStringmac ,formatHotkeyStringWin} from '../../utils/hotkeyUtils'; // 🔁 引入工具函数
|
||||
|
||||
import { UpHotkey, GetHotkeys } from '../../../bindings/cmbone/internal/services/hotkeyservice';
|
||||
import { IsmacOS } from '../../utils/osinfo'
|
||||
import { message } from 'ant-design-vue';
|
||||
const ismacos=ref(false)
|
||||
// const modelValue = ref(false)
|
||||
const OpenShortcut = ref('');
|
||||
const OpenSetting = ref('');
|
||||
|
||||
const isInitialized = ref(false);
|
||||
|
||||
let lastSaved = '';
|
||||
//const action = 'OpenSearch';
|
||||
// 监听变化自动保存
|
||||
watch(OpenShortcut, async (newShortcut) => {
|
||||
if (!isInitialized.value) return;
|
||||
|
||||
if (!newShortcut || newShortcut === lastSaved) return;
|
||||
|
||||
try {
|
||||
if(ismacos.value){
|
||||
await SendHanld(1,newShortcut);
|
||||
}else{
|
||||
await SendHanldWin(1,newShortcut);
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.success('快捷键保存失败: ' + e.message);
|
||||
}
|
||||
},{ flush: 'post' }
|
||||
);
|
||||
|
||||
|
||||
watch(OpenSetting, async (newShortcut) => {
|
||||
if (!isInitialized.value) return;
|
||||
|
||||
if (!newShortcut || newShortcut === lastSaved) return;
|
||||
try {
|
||||
if(ismacos.value){
|
||||
await SendHanld(2,newShortcut);
|
||||
}else{
|
||||
await SendHanldWin(2,newShortcut);
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.success('快捷键保存失败: ' + e.message);
|
||||
}
|
||||
},{ flush: 'post' }
|
||||
);
|
||||
|
||||
|
||||
const SendHanld = async (item,newShortcut) => {
|
||||
try {
|
||||
const parsed = parseShortcutToHotkey(newShortcut);
|
||||
if (!parsed) {
|
||||
console.error('❌ 快捷键格式错误:', newShortcut);
|
||||
return;
|
||||
}
|
||||
message.success('快捷键已保存');
|
||||
await UpHotkey(item,parsed.key, parsed.modifier);
|
||||
} catch (e: any) {
|
||||
message.success('快捷键保存失败: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const SendHanldWin = async (item,newShortcut) => {
|
||||
try {
|
||||
const parsed = parseShortcutToHotkeyWin(newShortcut);
|
||||
if (!parsed) {
|
||||
console.error('❌ 快捷键格式错误:', newShortcut);
|
||||
return;
|
||||
}
|
||||
message.success('快捷键已保存');
|
||||
await UpHotkey(item,parsed.key, parsed.modifier);
|
||||
} catch (e: any) {
|
||||
message.success('快捷键保存失败: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
interface HotkeyItem {
|
||||
id: number;
|
||||
keycode: number;
|
||||
modifiers: number;
|
||||
}
|
||||
const hotkeyentry = ref<HotkeyItem[]>([]);
|
||||
|
||||
const Gethotkey = async () => {
|
||||
hotkeyentry.value = await GetHotkeys();
|
||||
if (hotkeyentry.value && hotkeyentry.value.length > 0) {
|
||||
if(ismacos.value){
|
||||
OpenShortcut.value = formatHotkeyStringmac(hotkeyentry.value[0].keycode, hotkeyentry.value[0].modifiers);
|
||||
OpenSetting.value = formatHotkeyStringmac(hotkeyentry.value[1].keycode, hotkeyentry.value[1].modifiers);
|
||||
}
|
||||
else{
|
||||
OpenShortcut.value = formatHotkeyStringWin(hotkeyentry.value[0].keycode, hotkeyentry.value[0].modifiers);
|
||||
OpenSetting.value = formatHotkeyStringWin(hotkeyentry.value[1].keycode, hotkeyentry.value[1].modifiers);
|
||||
}
|
||||
await nextTick();
|
||||
isInitialized.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
onMounted(async() => {
|
||||
ismacos.value=IsmacOS()
|
||||
Gethotkey();
|
||||
// 等下一轮 DOM 渲染完成后才启用监听,避免初始赋值触发 watch
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-white" >{{ $t('components.general.title.shortcut') }}</h2>
|
||||
<div>
|
||||
<div class="bg-white rounded-lg shadow divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
|
||||
<ListItem :label="$t('components.general.label.open_second')" :subLabel="$t('components.general.subLabel.ht_shortcut')">
|
||||
<ShortcutInput v-if="ismacos" v-model:modelValue="OpenShortcut" />
|
||||
<WinShortcutInput v-else v-model:modelValue="OpenShortcut" />
|
||||
</ListItem>
|
||||
<ListItem :label="$t('components.general.label.open_setting')" subLabel="">
|
||||
<ShortcutInput v-if="ismacos" v-model:modelValue="OpenSetting" />
|
||||
<WinShortcutInput v-else v-model:modelValue="OpenSetting" />
|
||||
</ListItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
h2{
|
||||
text-align: left;
|
||||
margin:15px 15px 4px 15px;
|
||||
font-size:15px;
|
||||
}
|
||||
.rg_desc{
|
||||
text-align: left;
|
||||
margin:4px 0px 4px 8px;
|
||||
font-size:13px;
|
||||
color:#999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="flex h-full ">
|
||||
<!-- Left List -->
|
||||
<div class=" pl-1 pb-2 pr-1 flex flex-col" :class="currentTag === null ? 'w-3/5 ' : 'w-2/5'">
|
||||
<div class="mr-2 ml-1">
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-white float-left">
|
||||
{{ $t('components.tags.title.manage_tags') }}
|
||||
</h2>
|
||||
<button @click="createNew"
|
||||
class="w-[100px] float-right mb-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-2 py-2 rounded-xl flex items-center gap-2 font-medium transition-colors shadow-sm">
|
||||
<span class="text-xl pb-1">+</span> {{ $t('components.tags.buttons.newtag') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search -->
|
||||
<div class="relative w-full pr-2">
|
||||
<FilterOutlined class="absolute pl-1 top-[15px] -translate-y-1/2 text-gray-400 pointer-events-none" />
|
||||
<input v-model="keyword" placeholder="Search Tag..."
|
||||
class="w-full h-[30px] outline-none mb-2 pl-6 pr-3 py-1 border rounded-lg text-sm dark:bg-gray-900 dark:text-white dark:border-slate-500" />
|
||||
</div>
|
||||
<!-- List -->
|
||||
<div class="flex-1 overflow-auto space-y-1 scrollbar-thin">
|
||||
<div v-for="item in filteredTags" :key="item.id" class="item" :class="{ selected: currentTag?.id === item.id }"
|
||||
@click="select(item)">
|
||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded mr-1 ml-[-4px] mt-1"
|
||||
:class="item.color">
|
||||
<img :src="item.icon" class="h-8 w-8 object-contain" draggable="false" />
|
||||
</div>
|
||||
<div class="flex-1 ">
|
||||
<div class=" text-sm text-left line-clamp-1 break-words dark:text-white">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="text-sm text-left text-gray-500 line-clamp-2 dark:text-gray-300">
|
||||
{{ item.keywords.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider dashed plain>END</a-divider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Editor -->
|
||||
<div class="flex w-3/5 pt-2 pb-6 bg-white rounded-lg dark:bg-gray-800">
|
||||
<div v-if="currentTag" class="w-full">
|
||||
<div class="w-full mb-4 px-5 border-b gap-2 items-center dark:border-gray-700">
|
||||
<div class="float-right flex gap-2 pt-1 w-[20%] justify-end">
|
||||
<DeleteOutlined v-if="currentTag.is_default != 1 && currentTag.id != 0" @click="deleteTag"
|
||||
class="px-1 cursor-pointer float-left text-red-600" />
|
||||
<CheckCircleOutlined class=" px-1 cursor-pointer float-right text-green-600"
|
||||
@click="editTag(currentTag)" />
|
||||
</div>
|
||||
<div class="pb-3 itmes-left">
|
||||
<input v-model="currentTag.name"
|
||||
class="text-sm w-[80%] font-semibold outline-none dark:bg-gray-800 dark:text-white "
|
||||
placeholder="Tag Title" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 tracking-wider">{{ $t('components.tags.label.tagicon') }}</div>
|
||||
<!-- 当前预览 -->
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{{ $t('components.tags.label.current_preview') }}:</span>
|
||||
<div class="w-9 h-9 rounded-lg flex items-center justify-center text-white" :class="selectedColor">
|
||||
<img v-if="customIcon" :src="customIcon" class="w-6 h-6 object-contain" />
|
||||
<img v-else-if="selectedIconComponent" :src="selectedIconComponent" class="w-6 h-6 object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- icon 列表 -->
|
||||
<div class="flex items-center gap-3 flex-wrap ">
|
||||
<div v-for="item in iconOptions" :key="item.name" @click="selectIcon(item.name)"
|
||||
class="shrink-0 w-10 h-10 rounded-lg flex items-center justify-center cursor-pointer transition" :class="[
|
||||
selectedIcon === item.name && !customIcon
|
||||
? 'bg-indigo-600 text-white shadow'
|
||||
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
||||
]">
|
||||
<img :src="item.component" class="w-8 h-8 object-contain" />
|
||||
</div>
|
||||
<div class="w-px h-6 bg-gray-200 mx-1"></div>
|
||||
<label for="fileInput" @click="handleUpload"
|
||||
class="w-10 h-10 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<UploadOutlined class="text-gray-500 dark:text-gray-200" />
|
||||
</label>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<div class="text-xs text-gray-400 mt-2">
|
||||
SVG, PNG or JPG (max 100x100px recommended)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-xs text-left text-gray-500 dark:text-gray-400">
|
||||
{{ $t('components.tags.label.tagcolor') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div v-for="color in colors" :key="color" @click="selectedColor = color"
|
||||
class="w-5 h-5 rounded-full cursor-pointer border-2 shrink-0 relative"
|
||||
:class="[color, selectedColor === color ? 'ring-2 ring-white ring-offset-2 ring-offset-black/20 scale-100 ' : 'border-transparent']">
|
||||
<div v-if="color === 'bg-transparent'"
|
||||
class="absolute inset-0 bg-[linear-gradient(45deg,#ccc_25%,transparent_25%),linear-gradient(-45deg,#ccc_25%,transparent_25%),linear-gradient(45deg,transparent_75%,#ccc_75%),linear-gradient(-45deg,transparent_75%,#ccc_75%)] bg-[length:6px_6px] bg-[position:0_0,0_3px,3px_-3px,-3px_0px]">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TAG 子标签 -->
|
||||
<div class="px-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-xs text-left text-gray-500 dark:text-gray-400">{{ $t('components.tags.label.subtags') }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<input v-model="newSubTag" @keyup.enter="addSubTag"
|
||||
class="flex-1 h-[30px] bg-gray-100 rounded-lg px-3 py-2 outline-none text-sm dark:bg-gray-900 dark:text-white"
|
||||
placeholder="Add sub tag..." />
|
||||
<button @click="addSubTag" class="px-4 py bg-black text-white rounded-lg text-sm ">
|
||||
{{ $t('components.tags.buttons.add_subtag') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 标签列表 -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="(tag, index) in currentTag.keywords" :key="index"
|
||||
class="flex items-center gap-2 bg-gray-100 px-2 py-1.5 rounded-lg text-sm dark:bg-gray-900 dark:text-white">
|
||||
<span class="px-1">{{ tag }}</span>
|
||||
<button @click="removeSubTag(index)"
|
||||
class="w-4 h-4 flex items-center justify-center text-gray-400 hover:text-red-500 ">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="currentTag.keywords.length === 0" class="text-xs text-gray-400 mt-2">
|
||||
No sub tags yet
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right px-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-400 p-6 text-center mx-auto ">
|
||||
<img src="/system/mychoice.svg" class="mx-auto mb-4" style="width: 120px; height: 120px;" />
|
||||
Select a left tags to view details
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, createVNode } from 'vue'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { CheckCircleOutlined, DeleteOutlined, UploadOutlined, ExclamationCircleOutlined, FilterOutlined} from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
const keyword = ref('')
|
||||
//const tags = ref<{id:number, name:string, color:string}[]>([])
|
||||
const currentTag = ref<Tag | null>(null)
|
||||
interface Tag {
|
||||
id: number
|
||||
tag_key: string
|
||||
name: string
|
||||
color: string
|
||||
icon: string
|
||||
keywords: string[]
|
||||
is_default: number
|
||||
sort: number
|
||||
created_at?: number
|
||||
updated_at?: number
|
||||
}
|
||||
|
||||
const Tags = ref<Tag[]>([
|
||||
{
|
||||
id: 1,
|
||||
tag_key: 'work',
|
||||
name: 'Work',
|
||||
color: 'bg-blue-100',
|
||||
icon: '/visual/work.png',
|
||||
keywords: ['office', 'project', 'meeting'],
|
||||
is_default: 0,
|
||||
sort: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tag_key: 'life',
|
||||
name: 'Life',
|
||||
color: 'bg-green-100',
|
||||
icon: '/visual/eat.png',
|
||||
keywords: ['home', 'family', 'health'],
|
||||
is_default: 0,
|
||||
sort: 2
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tag_key: 'rest',
|
||||
name: 'Rest',
|
||||
color: 'bg-yellow-100',
|
||||
icon: '/visual/coffee.png',
|
||||
keywords: ['break', 'relax', 'leisure'],
|
||||
is_default: 0,
|
||||
sort: 3
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
tag_key: 'read',
|
||||
name: 'Read',
|
||||
color: 'bg-purple-100',
|
||||
icon: '/visual/read.png',
|
||||
keywords: ['book', 'article', 'study'],
|
||||
is_default: 0,
|
||||
sort: 4
|
||||
}
|
||||
])
|
||||
const filteredTags = computed(() => {
|
||||
return Tags.value.filter(t => t.name.toLowerCase().includes(keyword.value.toLowerCase()))
|
||||
})
|
||||
function createNew() {
|
||||
currentTag.value = null
|
||||
selectedColor.value = 'bg-slate-100'
|
||||
selectedIcon.value = 'Default'
|
||||
customIcon.value = null
|
||||
currentTag.value = {
|
||||
id: 0,
|
||||
tag_key: '',
|
||||
name: '',
|
||||
color: 'bg-slate-100',
|
||||
icon: '/visual/default.png',
|
||||
keywords: [],
|
||||
is_default: 0,
|
||||
sort: 0
|
||||
}
|
||||
}
|
||||
|
||||
function editTag(tag) {
|
||||
if (currentTag.value == null) return
|
||||
tag.color = selectedColor.value
|
||||
tag.icon = customIcon.value || selectedIconComponent.value || '/visual/default.png'
|
||||
if (tag.id == 0) {
|
||||
tag.Lang = i18n.global.locale.value
|
||||
tag.tag_key = tag.name.toLowerCase().replace(/\s+/g, '-')
|
||||
tag.id = Date.now() + Math.random()
|
||||
Tags.value.unshift(tag)
|
||||
currentTag.value = null
|
||||
} else {
|
||||
const index = Tags.value.findIndex(t => t.id === tag.id)
|
||||
if (index !== -1) {
|
||||
Tags.value[index] = tag
|
||||
}
|
||||
currentTag.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTag() {
|
||||
if (currentTag.value == null) return
|
||||
Modal.confirm({
|
||||
title: `Do you want to delete the tag "${currentTag.value?.name}"?`,
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: 'When clicked the OK button, this dialog will be closed after 1 second',
|
||||
okText: 'Yes',
|
||||
okType: 'danger',
|
||||
cancelText: 'No',
|
||||
async onOk() {
|
||||
try {
|
||||
Tags.value = Tags.value.filter(t => t.id !== currentTag.value?.id)
|
||||
message.success('Tag deleted successfully.')
|
||||
currentTag.value = null
|
||||
} catch (error) {
|
||||
message.error('Failed to delete the tag.')
|
||||
}
|
||||
},
|
||||
onCancel() { },
|
||||
});
|
||||
}
|
||||
// 选择
|
||||
function select(p: Tag) {
|
||||
selectedColor.value = p.color
|
||||
customIcon.value = p.icon
|
||||
currentTag.value = JSON.parse(JSON.stringify(p))
|
||||
}
|
||||
function fetchTags() {
|
||||
// 调用 API 获取标签列表
|
||||
}
|
||||
onMounted(async () => {
|
||||
await fetchTags()
|
||||
})
|
||||
|
||||
const selectedColor = ref('bg-slate-100') // 默认颜色
|
||||
const colors = [
|
||||
'bg-transparent',
|
||||
'bg-slate-100',
|
||||
'bg-gray-100',
|
||||
'bg-red-100',
|
||||
'bg-yellow-100',
|
||||
'bg-green-100',
|
||||
'bg-blue-100',
|
||||
'bg-indigo-100',
|
||||
'bg-purple-100',
|
||||
'bg-slate-400',
|
||||
'bg-red-400',
|
||||
'bg-yellow-400',
|
||||
'bg-green-400',
|
||||
'bg-blue-400',
|
||||
'bg-indigo-400',
|
||||
'bg-purple-400',
|
||||
'bg-red-700',
|
||||
'bg-yellow-700',
|
||||
'bg-green-700',
|
||||
'bg-zinc-800'
|
||||
]
|
||||
// icon 列表
|
||||
const iconOptions = [
|
||||
{ name: 'Default', component: '/visual/default.png' },
|
||||
{ name: 'Work', component: '/visual/work.png' },
|
||||
{ name: 'Life', component: '/visual/eat.png' },
|
||||
{ name: 'Rest', component: '/visual/coffee.png' },
|
||||
{ name: 'read', component: '/visual/read.png' },
|
||||
]
|
||||
|
||||
const selectedIcon = ref('Default')
|
||||
const customIcon = ref<string | null>(null)
|
||||
const selectedIconComponent = computed(() => {
|
||||
return iconOptions.find(i => i.name === selectedIcon.value)?.component
|
||||
})
|
||||
|
||||
function selectIcon(name: string) {
|
||||
selectedIcon.value = name
|
||||
customIcon.value = null
|
||||
}
|
||||
// 上传处理
|
||||
async function handleUpload(e: Event) {
|
||||
console.log('上传事件:')
|
||||
}
|
||||
|
||||
// 子标签
|
||||
const newSubTag = ref('')
|
||||
// 添加
|
||||
function addSubTag() {
|
||||
const val = newSubTag.value.trim()
|
||||
if (!val) return
|
||||
if (currentTag.value?.keywords.includes(val)) return
|
||||
currentTag.value?.keywords.push(val)
|
||||
newSubTag.value = ''
|
||||
}
|
||||
// 删除
|
||||
function removeSubTag(index: number) {
|
||||
currentTag.value?.keywords.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
/* 全局样式(可放在 main.css 或 tailwind.css) */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.item {
|
||||
@apply flex p-2 hover:bg-gray-100 cursor-pointer rounded dark:hover:bg-slate-700 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.selected {
|
||||
@apply border-l-4 border-indigo-500 bg-white dark:bg-slate-500 dark:border-slate-600;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@apply px-2 py-1 text-xs bg-gray-100 rounded mr-2;
|
||||
}
|
||||
|
||||
.tag.active {
|
||||
@apply bg-purple-100 text-purple-600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "My App",
|
||||
"login": "Login"
|
||||
},
|
||||
"appmenus": {
|
||||
"second": "Second win",
|
||||
"version": "Version: 0.3.0",
|
||||
"quit": "Quit",
|
||||
"preferences": "Settings"
|
||||
},
|
||||
"menus": {
|
||||
"about": "About",
|
||||
"general": "General",
|
||||
"shortcut": "Shortcut key",
|
||||
"second": "Second win",
|
||||
"setting": "Settings",
|
||||
"dashboard": "Dashboard",
|
||||
"tags": "Tags"
|
||||
},
|
||||
"components": {
|
||||
"general": {
|
||||
"title": {
|
||||
"application": "Apply settings",
|
||||
"exterior": "Appearance settings",
|
||||
"power": "Permission settings",
|
||||
"update": "Application update",
|
||||
"shortcut": "Shortcut key settings"
|
||||
},
|
||||
"label": {
|
||||
"assistant_access": "Accessibility permissions",
|
||||
"automatic_up": "Automatically check for updates",
|
||||
"disk_access": "Full disk access",
|
||||
"language": "Interface language",
|
||||
"next_up": "Automatically check for updates next time",
|
||||
"startup": "Started when logged in",
|
||||
"theme": "Theme mode",
|
||||
"open_second": "Open second window",
|
||||
"open_setting": "Open manages window"
|
||||
},
|
||||
"subLabel": {
|
||||
"sb_assistant_access": "Accessibility permission is required to operate clipboard contents",
|
||||
"sb_disk_access": "Full disk access is required to implement file preview",
|
||||
"ht_shortcut": "Press the combination key, which must contain at least one modifier key and one main key"
|
||||
},
|
||||
"buttons": {
|
||||
"authorized": "Auth",
|
||||
"go_authorized": "Auth now"
|
||||
}
|
||||
},
|
||||
"themesetting": {
|
||||
"select": {
|
||||
"opt_dark": "Dark",
|
||||
"opt_light": "Bright"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"version": "Version",
|
||||
"author": "Jin Gong",
|
||||
"description_p1": "Just do the right thing,",
|
||||
"description_p2": "at the right time."
|
||||
},
|
||||
"dashboard":{
|
||||
"title":{
|
||||
"upcoming_task":"Upcoming Tasks"
|
||||
},
|
||||
"label":{
|
||||
"total_tasks":"Total Tasks",
|
||||
"completed_tasks":"Completed Tasks",
|
||||
"pending_tasks":"Pending Tasks",
|
||||
"no_tasks":"No Tasks"
|
||||
},
|
||||
"buttons":{
|
||||
"newtask":"New Task",
|
||||
"upcoming_all":"All",
|
||||
"upcoming_priority":"Priority"
|
||||
}
|
||||
},
|
||||
"tags":{
|
||||
"title":{
|
||||
"manage_tags":"Tags"
|
||||
},
|
||||
"label":{
|
||||
"name":"Name",
|
||||
"tagcolor":"TAG COLOR",
|
||||
"tagicon":"TAG ICON",
|
||||
"current_preview":"CURRENT PREVIEW",
|
||||
"subtags":"SUB TAGS"
|
||||
},
|
||||
"buttons":{
|
||||
"newtag":"New Tag",
|
||||
"add_subtag":"Add"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
import zhHK from './zh-HK.json'
|
||||
export const availableLocales = ['en', 'zh','zh-HK'] as const
|
||||
export type Locale = typeof availableLocales[number]
|
||||
|
||||
export async function loadLocaleMessages(locale: Locale) {
|
||||
const messagesMap = {
|
||||
en,
|
||||
zh,
|
||||
'zh-HK':zhHK
|
||||
}
|
||||
return messagesMap[locale]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"appmenus":{
|
||||
"second": "第二窗口",
|
||||
"version": "版本: 0.3.0",
|
||||
"quit": "退出應用",
|
||||
"preferences": "偏好設定"
|
||||
},
|
||||
"menus": {
|
||||
"about": "關於",
|
||||
"backup": "數據備份",
|
||||
"general": "通用設置",
|
||||
"history": "歷史記錄",
|
||||
"remote": "遠程共享",
|
||||
"shortcut": "快速鍵",
|
||||
"second": "第二窗口",
|
||||
"setting": "設置",
|
||||
"dashboard": "儀表盤",
|
||||
"tags": "標籤"
|
||||
},
|
||||
"app": {
|
||||
"login": "登入",
|
||||
"title": "我的應用"
|
||||
},
|
||||
"components": {
|
||||
"general": {
|
||||
"buttons": {
|
||||
"authorized": "已授權",
|
||||
"go_authorized": "去授權"
|
||||
},
|
||||
"label": {
|
||||
"assistant_access": "輔助功能訪問權限",
|
||||
"automatic_up": "自動檢查更新",
|
||||
"disk_access": "完全磁盤訪問權限",
|
||||
"language": "界面語言",
|
||||
"next_up": "下次啟動自動檢查更新",
|
||||
"startup": "登錄時啟動",
|
||||
"theme": "主題模式",
|
||||
"open_second": "打開第二窗口",
|
||||
"open_setting": "打開管理窗口"
|
||||
},
|
||||
"subLabel": {
|
||||
"sb_assistant_access": "需要無障礙訪問權限來操作剪切板內容",
|
||||
"sb_disk_access": "需要完全磁盤訪問權限來實現文件預覽",
|
||||
"ht_shortcut": "按下組合鍵,必須包含至少一個修飾鍵和一個主鍵"
|
||||
},
|
||||
"title": {
|
||||
"application": "應用設置",
|
||||
"exterior": "外觀設置",
|
||||
"power": "權限設置",
|
||||
"update": "應用更新"
|
||||
}
|
||||
},
|
||||
"themesetting": {
|
||||
"select": {
|
||||
"opt_dark": "暗色模式",
|
||||
"opt_light": "亮色模式"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"version": "版本",
|
||||
"author": "Jin Gong",
|
||||
"description_p1": "只是在合適的時候,",
|
||||
"description_p2": "做一些合適的事。"
|
||||
},
|
||||
"dashboard":{
|
||||
"title":{
|
||||
"upcoming_task": "即將到來的任務"
|
||||
},
|
||||
"label":{
|
||||
"total_tasks":"總任務數",
|
||||
"completed_tasks":"已完成任務",
|
||||
"pending_tasks":"待辦任務",
|
||||
"no_tasks":"暫無任務"
|
||||
},
|
||||
"buttons":{
|
||||
"newtask":"新建任務",
|
||||
"upcoming_all":"全部",
|
||||
"upcoming_priority":"優先"
|
||||
}
|
||||
},
|
||||
"tags":{
|
||||
"title":{
|
||||
"manage_tags":"標籤"
|
||||
},
|
||||
"label":{
|
||||
"icon":"標籤圖示",
|
||||
"current_preview":"當前預覽",
|
||||
"tagcolor":"標籤顏色",
|
||||
"subtags":"子標籤"
|
||||
},
|
||||
"buttons":{
|
||||
"newtag":"新建標籤",
|
||||
"add_subtag":"添加"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "我的应用",
|
||||
"login": "登录"
|
||||
},
|
||||
"appmenus": {
|
||||
"second": "第二窗口",
|
||||
"preferences": "偏好设置",
|
||||
"version": "版本:0.3.0",
|
||||
"quit": "退出应用"
|
||||
},
|
||||
"menus":{
|
||||
"dashboard":"仪表盘",
|
||||
"general":"通用设置",
|
||||
"shortcut":"快捷键",
|
||||
"about":"关于",
|
||||
"second":"第二窗口",
|
||||
"setting":"设置",
|
||||
"tags":"标签"
|
||||
},
|
||||
"components":{
|
||||
"home":{},
|
||||
"general":{
|
||||
"title":{
|
||||
"power":"权限设置",
|
||||
"application":"应用设置",
|
||||
"exterior":"外观设置",
|
||||
"update":"应用更新",
|
||||
"shortcut":"快捷键设置"
|
||||
},
|
||||
"label":{
|
||||
"assistant_access":"辅助功能访问权限",
|
||||
"disk_access":"完全磁盘访问权限",
|
||||
"startup" :"登录时启动",
|
||||
"language":"界面语言",
|
||||
"theme":"主题模式",
|
||||
"automatic_up":"自动检查更新",
|
||||
"next_up":"下次启动自动检查更新",
|
||||
"open_second":"打开第二窗口",
|
||||
"open_setting":"打开管理窗口"
|
||||
},
|
||||
"subLabel":{
|
||||
"sb_assistant_access":"需要无障碍访问权限来操作剪切板内容",
|
||||
"sb_disk_access":"需要完全磁盘访问权限来实现文件预览",
|
||||
"ht_shortcut":"按下组合键,必须包含至少一个修饰键和一个主键"
|
||||
},
|
||||
"buttons":{
|
||||
"authorized":"已授权",
|
||||
"go_authorized":"去授权"
|
||||
}
|
||||
},
|
||||
"themesetting":{
|
||||
"select":{
|
||||
"opt_light":"亮色模式",
|
||||
"opt_dark":"暗色模式"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"version": "版本",
|
||||
"author": "Jin Gong",
|
||||
"description_p1": "只是在合适的时候,",
|
||||
"description_p2": "做一些合适的事。"
|
||||
},
|
||||
"dashboard":{
|
||||
"title":{
|
||||
"upcoming_task":"即将进行的任务"
|
||||
},
|
||||
"label":{
|
||||
"total_tasks":"总任务数",
|
||||
"completed_tasks":"已完成任务",
|
||||
"pending_tasks":"待办任务",
|
||||
"no_tasks":"暂无任务"
|
||||
},
|
||||
"buttons":{
|
||||
"newtask":"新建任务",
|
||||
"upcoming_all":"全部",
|
||||
"upcoming_priority":"优先"
|
||||
}
|
||||
},
|
||||
"tags":{
|
||||
"title":{
|
||||
"manage_tags":"标签"
|
||||
},
|
||||
"label":{
|
||||
"name":"名称",
|
||||
"tagcolor":"标签颜色",
|
||||
"tagicon":"标签图标",
|
||||
"current_preview":"当前预览",
|
||||
"subtags":"子标签"
|
||||
},
|
||||
"buttons":{
|
||||
"newtag":"新增标签",
|
||||
"add_subtag":"添加",
|
||||
"delete_tag":"删除标签",
|
||||
"delete_subtag":"删除子标签"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import Antd from 'ant-design-vue';//add
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import {i18n,setupI18n } from './utils/i18n'
|
||||
import { initTheme } from './utils/ThemeManager'//add
|
||||
import router from './router/index'
|
||||
import { GetLanguage } from '../bindings/cmbone/internal/services/appconfigservice'
|
||||
|
||||
initTheme()
|
||||
if (window.location.hash === '' || window.location.hash === '#/') {
|
||||
router.replace('/second')
|
||||
}
|
||||
async function bootstrap(){
|
||||
await setupI18n('zh')//默认语言或从后端读取
|
||||
createApp(App).use(Antd).use(router).use(i18n).mount('#app')
|
||||
router.isReady().then(async () => {
|
||||
const lang = await GetLanguage() //从后端读取语言设置 lang as any|| 'zh'
|
||||
await setupI18n(lang as any|| 'zh')
|
||||
})
|
||||
}
|
||||
bootstrap()
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { applyTheme } from '../../utils/ThemeManager'
|
||||
import { RecognizeImageBase64 } from '../../../bindings/cmbone/internal/services/ocrservice'
|
||||
const bc = new BroadcastChannel('theme')
|
||||
bc.onmessage = (e) => {
|
||||
applyTheme(e.data)
|
||||
}
|
||||
|
||||
//-- OCR功能 ---
|
||||
declare global {
|
||||
interface Window {
|
||||
go: any
|
||||
}
|
||||
}
|
||||
|
||||
const imageUrl = ref<string>('')
|
||||
const imageBase64 = ref<string>('')
|
||||
const result = ref<string>('')
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// 文件处理
|
||||
function handleFile(file: File) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string
|
||||
imageUrl.value = base64
|
||||
imageBase64.value = base64.split(',')[1]
|
||||
result.value = '' // 清空旧结果
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// 点击选择
|
||||
function selectImage() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
if (file) handleFile(file)
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
// 拖拽
|
||||
function onDrop(e: DragEvent) {
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file) handleFile(file)
|
||||
}
|
||||
|
||||
// OCR 调用
|
||||
async function handleOCR() {
|
||||
if (!imageBase64.value) return
|
||||
loading.value = true
|
||||
result.value = ''
|
||||
try {
|
||||
const res = await RecognizeImageBase64(imageBase64.value)
|
||||
result.value = res || '未识别到内容'
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
result.value = '识别失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-start justify-center pt-8 h-screen w-screen h-full w-full font-sans select-none bg-gray-100 drag-region dark:bg-gray-900">
|
||||
<TransitionGroup name="card" tag="div">
|
||||
<div
|
||||
class="flex-1 w-[300px] rounded-2xl bg-white/80 dark:bg-gray-700 dark:text-white backdrop-blur-md px-4 py-2 shadow-[0_8px_24px_rgba(0,0,0,0.12)] transition">
|
||||
<!-- 图片区域 -->
|
||||
<div
|
||||
class="h-20 w-full border-2 border-dashed border-gray-300 rounded-xl flex items-center justify-center cursor-pointer overflow-hidden hover:border-blue-400 transition"
|
||||
@click="selectImage" @drop.prevent="onDrop" @dragover.prevent>
|
||||
<img v-if="imageUrl" :src="imageUrl" class="w-full h-full object-cover" />
|
||||
<span v-else class="text-gray-400 text-sm">
|
||||
点击或者拖拽图片
|
||||
</span>
|
||||
</div>
|
||||
<button @click="handleOCR" :disabled="!imageBase64 || loading" class="w-full my-2 py-1 rounded-lg text-white transition
|
||||
bg-blue-500 hover:bg-blue-600
|
||||
disabled:bg-gray-300 disabled:cursor-not-allowed">
|
||||
<span v-if="loading">识别中...</span>
|
||||
<span v-else>OCR识别</span>
|
||||
</button>
|
||||
<div v-if="result" class="bg-gray-50 rounded-lg p-3 text-sm text-gray-700 max-h-40 overflow-auto dark:bg-gray-800 dark:text-gray-300">
|
||||
<div class="font-medium mb-1">识别结果:</div>
|
||||
<pre class="whitespace-pre-wrap">{{ result }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createRouter, createWebHashHistory} from 'vue-router'
|
||||
import MainPage from '../components/Main/Index.vue'
|
||||
import SecondPage from '../pages/Home/Index.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: MainPage },
|
||||
{ path: '/second', component: SecondPage }
|
||||
]
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(), // Use createWebHashHistory for hash-based routing
|
||||
routes
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,55 @@
|
||||
// src/utils/ThemeManager.ts
|
||||
import { theme } from 'ant-design-vue'
|
||||
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const lightTheme = {
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}
|
||||
|
||||
export const darkTheme = {
|
||||
algorithm: theme.darkAlgorithm,
|
||||
}
|
||||
|
||||
const THEME_KEY = 'theme'
|
||||
export type ThemeMode = 'light' | 'dark' | 'systemdefault'
|
||||
export function applyTheme(mode: ThemeMode) {
|
||||
let resolvedTheme = mode
|
||||
if (mode === 'systemdefault') {
|
||||
resolvedTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
document.documentElement.classList.remove('dark', 'light')
|
||||
document.documentElement.classList.add(resolvedTheme)
|
||||
|
||||
themeConfig.value = getAntdCurrentTheme(resolvedTheme)
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
const savedTheme = (localStorage.getItem(THEME_KEY) || 'light') as ThemeMode
|
||||
applyTheme(savedTheme)
|
||||
|
||||
// 监听系统变化,仅在 systemdefault 时响应
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
const current = getCurrentTheme()
|
||||
if (current === 'systemdefault') {
|
||||
applyTheme('systemdefault')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setTheme(mode: ThemeMode) {
|
||||
localStorage.setItem(THEME_KEY, mode)
|
||||
applyTheme(mode)
|
||||
}
|
||||
|
||||
export function getCurrentTheme(): ThemeMode {
|
||||
return (localStorage.getItem(THEME_KEY) || 'light') as ThemeMode
|
||||
}
|
||||
// 全局响应式主题状态
|
||||
//export const themeMode = ref<ThemeMode>(getCurrentTheme())
|
||||
export const currentMode = ref<'light' | 'dark'>(localStorage.getItem('theme') as 'light' | 'dark' || 'light')
|
||||
export const themeConfig = ref<ThemeConfig>(getAntdCurrentTheme(currentMode.value))
|
||||
export function getAntdCurrentTheme(mode: ThemeMode): ThemeConfig {
|
||||
return mode === 'dark' ? darkTheme : lightTheme
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// hotkeyUtils.ts
|
||||
|
||||
const modifierMap: Record<string, number> = {
|
||||
Control: 256,
|
||||
Alt: 512,
|
||||
Shift: 1024,
|
||||
Meta: 2048,
|
||||
};
|
||||
|
||||
// 修饰键位定义(macOS 通常使用)
|
||||
export const MODIFIERS = {
|
||||
CMD: 1 << 8, // 256
|
||||
SHIFT: 1 << 9, // 512
|
||||
ALT: 1 << 10, // 1024
|
||||
CTRL: 1 << 11, // 2048
|
||||
};
|
||||
|
||||
// 快捷键字符串 => 键码映射(mac keycode 映射)
|
||||
const keyMap: Record<string, number> = {
|
||||
A: 0, B: 11, C: 8, D: 2, E: 14, F: 3, G: 5,
|
||||
H: 4, I: 34, J: 38, K: 40, L: 37, M: 46,
|
||||
N: 45, O: 31, P: 35, Q: 12, R: 15, S: 1,
|
||||
T: 17, U: 32, V: 9, W: 13, X: 7, Y: 16, Z: 6,
|
||||
};
|
||||
|
||||
|
||||
export function parseHotkeyString(hotkey: string): { key: number, modifier: number } | null {
|
||||
if (!hotkey.includes('+')) return null;
|
||||
|
||||
const parts = hotkey.split('+');
|
||||
const mods = parts.slice(0, -1); // 修饰键
|
||||
const keyStr = parts[parts.length - 1];
|
||||
|
||||
let modifier = 0;
|
||||
for (const mod of mods) {
|
||||
if (modifierMap[mod]) modifier |= modifierMap[mod];
|
||||
}
|
||||
|
||||
const key = keyStr.toUpperCase().charCodeAt(0); // A → 65, B → 66, S → 83 ...
|
||||
if (!key || isNaN(key)) return null;
|
||||
|
||||
return { key, modifier };
|
||||
}
|
||||
|
||||
export function parseShortcutToHotkey(shortcut: string): { key: number, modifier: number } {
|
||||
const parts = shortcut.toUpperCase().split('+');
|
||||
let keys = '';
|
||||
let modifier = 0;
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part) {
|
||||
case 'CMD':
|
||||
case 'COMMAND':
|
||||
case 'META':
|
||||
modifier |= 1 << 8;
|
||||
break;
|
||||
case 'SHIFT':
|
||||
modifier |= 1 << 9;
|
||||
break;
|
||||
case 'ALT':
|
||||
case 'OPTION':
|
||||
modifier |= 1 << 10;
|
||||
break;
|
||||
case 'CTRL':
|
||||
case 'CONTROL':
|
||||
modifier |= 1 << 11;
|
||||
break;
|
||||
default:
|
||||
keys = part;
|
||||
}
|
||||
}
|
||||
|
||||
// const keyMap: Record<string, number> = {
|
||||
// A: 0, B: 11, C: 8, D: 2, E: 14, F: 3, G: 5,
|
||||
// H: 4, I: 34, J: 38, K: 40, L: 37, M: 46,
|
||||
// N: 45, O: 31, P: 35, Q: 12, R: 15, S: 1,
|
||||
// T: 17, U: 32, V: 9, W: 13, X: 7, Y: 16, Z: 6,
|
||||
// };
|
||||
|
||||
const key = keyMap[keys];
|
||||
return {
|
||||
key,
|
||||
modifier,
|
||||
};
|
||||
}
|
||||
|
||||
// 反向映射:keycode => 字符
|
||||
const reverseKeyMap = Object.fromEntries(
|
||||
Object.entries(keyMap).map(([k, v]) => [v, k])
|
||||
);
|
||||
// 将 keycode 和 modifier 转换为字符串(如 40, 768 => "Cmd+Shift+K")
|
||||
export function formatHotkeyString(key: number, modifier: number): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (modifier & MODIFIERS.CMD) parts.push('Cmd');
|
||||
if (modifier & MODIFIERS.SHIFT) parts.push('Shift');
|
||||
if (modifier & MODIFIERS.ALT) parts.push('Alt');
|
||||
if (modifier & MODIFIERS.CTRL) parts.push('Ctrl');
|
||||
|
||||
const keyStr = reverseKeyMap[key];
|
||||
if (!keyStr) throw new Error(`Unknown keycode: ${key}`);
|
||||
|
||||
parts.push(keyStr.toUpperCase());
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
export function formatHotkeyStringmac(keycode: number, modifiers: number): string {
|
||||
const keyMap: Record<number, string> = {
|
||||
0: 'A', 11: 'B', 8: 'C', 2: 'D', 14: 'E', 3: 'F', 5: 'G',
|
||||
4: 'H', 34: 'I', 38: 'J', 40: 'K', 37: 'L', 46: 'M',
|
||||
45: 'N', 31: 'O', 35: 'P', 12: 'Q', 15: 'R', 1: 'S',
|
||||
17: 'T', 32: 'U', 9: 'V', 13: 'W', 7: 'X', 16: 'Y', 6: 'Z'
|
||||
};
|
||||
|
||||
const mods: string[] = [];
|
||||
|
||||
if (modifiers & (1 << 8)) mods.push('Cmd'); // mac
|
||||
if (modifiers & (1 << 9)) mods.push('Shift');
|
||||
if (modifiers & (1 << 10)) mods.push('Alt');
|
||||
if (modifiers & (1 << 11)) mods.push('Control');
|
||||
|
||||
const key = keyMap[keycode] ?? 'Unknown';
|
||||
|
||||
return [...mods, key].join('+');
|
||||
}
|
||||
|
||||
//win
|
||||
export function formatHotkeyStringWin(keycode: number, modifiers: number): string {
|
||||
const keyMap: Record<number, string> = {
|
||||
65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G',
|
||||
72: 'H', 73: 'I', 74: 'J', 75: 'K', 76: 'L', 77: 'M', 78: 'N',
|
||||
79: 'O', 80: 'P', 81: 'Q', 82: 'R', 83: 'S', 84: 'T', 85: 'U',
|
||||
86: 'V', 87: 'W', 88: 'X', 89: 'Y', 90: 'Z',
|
||||
13: 'Enter', 27: 'Esc', 32: 'Space',
|
||||
112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6',
|
||||
118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12',
|
||||
37: 'Left', 38: 'Up', 39: 'Right', 40: 'Down',
|
||||
};
|
||||
|
||||
const mods: string[] = [];
|
||||
|
||||
if (modifiers & 1) mods.push('Alt');
|
||||
if (modifiers & 2) mods.push('Ctrl');
|
||||
if (modifiers & 4) mods.push('Shift');
|
||||
if (modifiers & 8) mods.push('Win');
|
||||
|
||||
const key = keyMap[keycode] ?? `KeyCode(${keycode})`;
|
||||
|
||||
return [...mods, key].join('+');
|
||||
}
|
||||
|
||||
|
||||
export function parseShortcutToHotkeyWin(shortcut: string): { key: number, modifier: number } {
|
||||
const parts = shortcut.toUpperCase().split('+');
|
||||
let keyPart = '';
|
||||
let modifier = 0;
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part) {
|
||||
case 'ALT':
|
||||
modifier |= 1;
|
||||
break;
|
||||
case 'CTRL':
|
||||
case 'CONTROL':
|
||||
modifier |= 2;
|
||||
break;
|
||||
case 'SHIFT':
|
||||
modifier |= 4;
|
||||
break;
|
||||
case 'WIN':
|
||||
case 'CMD':
|
||||
case 'META':
|
||||
modifier |= 8;
|
||||
break;
|
||||
default:
|
||||
keyPart = part;
|
||||
}
|
||||
}
|
||||
|
||||
const key = keyWinMap[keyPart] ?? parseInt(keyPart);
|
||||
|
||||
return {
|
||||
key,
|
||||
modifier,
|
||||
};
|
||||
}
|
||||
|
||||
// 快捷键字符串 => 键码映射(win keycode 映射)
|
||||
const keyWinMap: Record<string, number> = {
|
||||
A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71,
|
||||
H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78,
|
||||
O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85,
|
||||
V: 86, W: 87, X: 88, Y: 89, Z: 90,
|
||||
ENTER: 13, ESC: 27, SPACE: 32,
|
||||
F1: 112, F2: 113, F3: 114, F4: 115,
|
||||
F5: 116, F6: 117, F7: 118, F8: 119,
|
||||
F9: 120, F10: 121, F11: 122, F12: 123,
|
||||
UP: 38, DOWN: 40, LEFT: 37, RIGHT: 39,
|
||||
};
|
||||