chore: migrate and structure cmbone project

This commit is contained in:
QiuSW
2026-07-08 15:13:26 +08:00
parent 519a532038
commit e5d10577c0
123 changed files with 9076 additions and 25 deletions
+20 -24
View File
@@ -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*
+21
View File
@@ -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.
+62 -1
View File
@@ -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`。
+33
View File
@@ -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}}
+85
View File
@@ -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 .
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+63
View File
@@ -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
+81
View File
@@ -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}}'
Binary file not shown.
+119
View File
@@ -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}}'
+35
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
#!/bin/bash
+1
View File
@@ -0,0 +1 @@
#!/bin/bash
+1
View File
@@ -0,0 +1 @@
#!/bin/bash
+1
View File
@@ -0,0 +1 @@
#!/bin/bash
+98
View File
@@ -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'
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+112
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

+93
View File
@@ -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.
+18
View File
@@ -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");
}
+6
View File
@@ -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";
+37
View File
@@ -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>);
}
}
+6
View File
@@ -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";
+35
View File
@@ -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);
+6
View File
@@ -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";
+31
View File
@@ -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>);
}
}
+81
View File
@@ -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>
+3288
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.
+145
View File
@@ -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);
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

+1
View File
@@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+1
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+39
View File
@@ -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>
+108
View File
@@ -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>
+171
View File
@@ -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>
+70
View File
@@ -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>
+205
View File
@@ -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>
+144
View File
@@ -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>
+384
View File
@@ -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>
+96
View File
@@ -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"
}
}
}
}
+16
View File
@@ -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]
}
+97
View File
@@ -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":"添加"
}
}
}
}
+99
View File
@@ -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":"删除子标签"
}
}
}
}
+23
View File
@@ -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()
+102
View File
@@ -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>
+13
View File
@@ -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
})
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+55
View File
@@ -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
}
+199
View File
@@ -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,
};

Some files were not shown because too many files have changed in this diff Show More