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
+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,
};
+19
View File
@@ -0,0 +1,19 @@
// src/i18n.ts
import { createI18n } from 'vue-i18n'
import { Locale, loadLocaleMessages } from '../locales'
const defaultLocale: Locale = 'zh'
export const i18n = createI18n({
legacy: false, // 使用 Composition API
locale: defaultLocale, // 默认语言
fallbackLocale: 'en',
messages: {},
})
//export default i18n
// 初始化语言(只加载一次)
export async function setupI18n(locale: Locale) {
const messages = await loadLocaleMessages(locale)
i18n.global.setLocaleMessage(locale, messages)
i18n.global.locale.value = locale
}
+30
View File
@@ -0,0 +1,30 @@
import { GetOS } from "../../bindings/cmbone/internal/services/systeminfo";
let cachedOS: string | null = null;
let osInitPromise: Promise<void>
/**
* 初始化 OS,只调用一次
*/
export function initOS(): Promise<void> {
if (!osInitPromise) {
osInitPromise = GetOS().then(os => {
cachedOS = os
})
}
return osInitPromise;
}
export function getOS(): string | null {
if (!cachedOS) {
throw new Error("OS not initialized yet, await OS_READY first")
}
return cachedOS;
}
export function IsmacOS(): boolean {
return cachedOS==='darwin';
}
// ✅ 导出一个初始化状态 Promise
export const OS_READY = initOS()
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+10
View File
@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: ["./index.html", "./src/**/*.{vue,js,ts}"],
theme: {
extend: {},
},
plugins: [],
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"noUnusedParameters": false,
"noImplicitAny": false,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})