Compare commits
8 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
063cf3e0be | 1 month ago |
|
|
ddc00a6ef9 | 2 months ago |
|
|
af91b0052a | 4 months ago |
|
|
9a9eef5a7c | 4 months ago |
|
|
fe33e9fd1a | 4 months ago |
|
|
9a5776d586 | 4 months ago |
|
|
f1833df97b | 5 months ago |
|
|
403e6608d3 | 5 months ago |
112 changed files with 2942 additions and 1290 deletions
@ -0,0 +1,113 @@ |
|||||
|
# https://zhuanlan.zhihu.com/p/164901026 |
||||
|
# https://www.antmoe.com/posts/18c087cf/ |
||||
|
# https://zhuanlan.zhihu.com/p/348712087 |
||||
|
# https://cloud.tencent.com/developer/article/1949574 |
||||
|
# 此工作流的名字 |
||||
|
name: Build |
||||
|
# 工作流的执行时机,可以设定为定时执行,每次push后执行,手动执行等 |
||||
|
on: |
||||
|
# workflow_dispatch为在Github仓库的Actions面板中手动执行 |
||||
|
# workflow_dispatch: |
||||
|
push: |
||||
|
branches: |
||||
|
- master |
||||
|
# 工作/任务,这里的工作是可以并行的。 |
||||
|
jobs: |
||||
|
# 工作的名称“编译windows版” |
||||
|
build: |
||||
|
# 运行的操作系统 windows |
||||
|
runs-on: ${{ matrix.os }} |
||||
|
env: |
||||
|
GH_TOKEN: ${{ secrets.ELECTRON_TOKEN }} |
||||
|
IS_ACTIONS: true |
||||
|
strategy: |
||||
|
matrix: |
||||
|
# https://www.likecs.com/ask-314443.html |
||||
|
node-version: [18.17.1] |
||||
|
os: [windows-2022, ubuntu-latest] |
||||
|
# 步骤 |
||||
|
steps: |
||||
|
# 使用预制action:拉取最新的代码 |
||||
|
- uses: actions/checkout@v3 |
||||
|
with: |
||||
|
ref: master |
||||
|
# https://pnpm.io/zh/continuous-integration/#github-actions |
||||
|
- uses: pnpm/action-setup@v2.2.4 |
||||
|
name: Install pnpm |
||||
|
id: pnpm-install |
||||
|
with: |
||||
|
version: 8.7.6 |
||||
|
# 安装node |
||||
|
- name: Use Node.js ${{ matrix.node-version }} |
||||
|
uses: actions/setup-node@v3 |
||||
|
with: |
||||
|
node-version: ${{ matrix.node-version }} |
||||
|
cache: 'pnpm' |
||||
|
- name: Install dependencies |
||||
|
run: pnpm install |
||||
|
# 安装python |
||||
|
- name: Use Python 3.9.13 |
||||
|
uses: actions/setup-python@v4 |
||||
|
with: |
||||
|
python-version: 3.9.13 |
||||
|
env: |
||||
|
PYTHON_VERSION: 3.9.13 |
||||
|
# https://docs.microsoft.com/zh-cn/visualstudio/releases/2017/vs2017-system-requirements-vs |
||||
|
# 将windows设置成windows-2016,2016要取消支持了,可换成2022 |
||||
|
# - name: set msvs |
||||
|
# run: npm config set msvs_version 2022 |
||||
|
# https://github.com/wxWidgets/wxWidgets/blob/master/.github/workflows/ci_msw.yml |
||||
|
# https://github.com/microsoft/setup-msbuild |
||||
|
- name: Add msbuild to PATH |
||||
|
if: matrix.os == 'windows-2022' |
||||
|
uses: microsoft/setup-msbuild@v1.1 |
||||
|
with: |
||||
|
vs-prerelease: true |
||||
|
# 步骤一的名称: |
||||
|
- name: Build |
||||
|
# 该步骤运行的终端命令,运行编译命令 |
||||
|
run: npm run build |
||||
|
# 步骤二的名称:将编译后的结果上传 |
||||
|
# - name: Upload File |
||||
|
# # 使用预制action:上传文件,可以将执行路径打包成zip上传 |
||||
|
# uses: actions/upload-artifact@v3 |
||||
|
# with: |
||||
|
# # 上传后文件的名称 |
||||
|
# name: windows |
||||
|
# # 打包的路径以及文件过滤,此为仅打包dist目录下的exe文件 |
||||
|
# path: out/*exe |
||||
|
- name: 读取当前版本号 |
||||
|
id: version |
||||
|
uses: ashley-taylor/read-json-property-action@v1.1 |
||||
|
with: |
||||
|
path: ./dist/package.json |
||||
|
property: version |
||||
|
- name: 读取描述文件 |
||||
|
id: description |
||||
|
uses: juliangruber/read-file-action@v1 |
||||
|
with: |
||||
|
path: ./changelog/${{steps.version.outputs.value}}.md |
||||
|
# step5: cleanup artifacts in dist_electron |
||||
|
# - name: 清理不必要的资产 |
||||
|
# run: | |
||||
|
# npx rimraf "out/!(*.exe|*.dmg)" |
||||
|
# - name: Generate release tag |
||||
|
# id: tag |
||||
|
# run: | |
||||
|
# echo "::set-output name=release_tag::UserBuild_$(date +"%Y.%m.%d_%H-%M")" |
||||
|
- name: Generate release tag |
||||
|
id: tag |
||||
|
run: | |
||||
|
echo "::set-output name=release_tag::v${{steps.version.outputs.value}}" |
||||
|
# echo "release_tag=v${{steps.version.outputs.value}}" >> $GITHUB_OUTPUT |
||||
|
- name: release # https://github.com/softprops/action-gh-release/issues/20 |
||||
|
uses: softprops/action-gh-release@v1 |
||||
|
with: |
||||
|
tag_name: ${{ steps.tag.outputs.release_tag }} |
||||
|
name: ${{ steps.tag.outputs.release_tag }} |
||||
|
files: "out/*exe,out/*dmg,out/*AppImage,out/*yml" |
||||
|
body: ${{steps.description.outputs.content}} |
||||
|
draft: false |
||||
|
prerelease: false |
||||
|
env: |
||||
|
GITHUB_TOKEN: ${{ secrets.ELECTRON_TOKEN }} |
||||
@ -0,0 +1,3 @@ |
|||||
|
## 0.0.1 |
||||
|
|
||||
|
第一个版本 |
||||
@ -1,3 +1,5 @@ |
|||||
|
// @ts-nocheck window.api 不需要检查
|
||||
|
|
||||
import { IApiClient } from "./abstract" |
import { IApiClient } from "./abstract" |
||||
|
|
||||
export class ElectronApiClient implements IApiClient { |
export class ElectronApiClient implements IApiClient { |
||||
@ -1,27 +1,13 @@ |
|||||
import { injectable } from "inversify" |
|
||||
import Setting from "setting/main" |
import Setting from "setting/main" |
||||
import { CustomAdapter, CustomLow } from "./custom" |
import { CustomAdapter, CustomLow } from "./custom" |
||||
import path from "node:path" |
import path from "node:path" |
||||
import BaseClass from "main/base/base" |
// import _logger from "logger/main"
|
||||
import _logger from "logger/main" |
|
||||
|
|
||||
const logger = _logger.createNamespace("db") |
// const logger = _logger.createNamespace("db")
|
||||
|
|
||||
@injectable() |
class DB { |
||||
class DB extends BaseClass { |
|
||||
destroy() { |
|
||||
logger.debug(`DB destroy`) |
|
||||
} |
|
||||
Modules: Record<string, CustomLow<any>> = {} |
Modules: Record<string, CustomLow<any>> = {} |
||||
|
|
||||
constructor() { |
|
||||
super() |
|
||||
} |
|
||||
|
|
||||
init() { |
|
||||
console.log("DB Init") |
|
||||
} |
|
||||
|
|
||||
create(filepath) { |
create(filepath) { |
||||
const adapter = new CustomAdapter<any>(filepath) |
const adapter = new CustomAdapter<any>(filepath) |
||||
const db = new CustomLow<object>(adapter, {}) |
const db = new CustomLow<object>(adapter, {}) |
||||
@ -1,3 +1,15 @@ |
|||||
|
import { UpdateInfo } from "electron-updater" |
||||
|
|
||||
|
export interface UpdaterCommand { |
||||
|
checkForUpdates: () => void |
||||
|
} |
||||
|
|
||||
export type EventMaps = { |
export type EventMaps = { |
||||
"update-progress": (data: { percent: number; all: number; now: number }) => void |
"update-progress": (data: { speed: number; percent: number; all: number; now: number }) => void |
||||
|
error: (err: any) => void |
||||
|
"updater:error": (info: UpdateInfo) => void |
||||
|
"checking-for-update": () => void |
||||
|
"update-available": (info: UpdateInfo) => void |
||||
|
"update-not-available": (info: UpdateInfo) => void |
||||
|
"updater:downloaded": (p: any) => void |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,7 @@ |
|||||
|
import type { IOnFunc } from "setting/main" |
||||
|
|
||||
|
export type EventMaps = { |
||||
|
init: IOnFunc |
||||
|
update: IOnFunc |
||||
|
change: (key: string, value: any) => void |
||||
|
} |
||||
@ -1,6 +1,4 @@ |
|||||
import { buildEmitter } from "base/event/main" |
import { buildEmitter } from "base/event/main" |
||||
import type { IOnFunc } from "setting/main" |
import { EventMaps } from "setting/common" |
||||
|
|
||||
export const emitter = buildEmitter<{ |
export const emitter = buildEmitter<EventMaps>() |
||||
update: IOnFunc |
|
||||
}>() |
|
||||
|
|||||
@ -1,5 +0,0 @@ |
|||||
import { PlatForm } from "." |
|
||||
|
|
||||
export function usePlatForm() { |
|
||||
return PlatForm.getInstance<PlatForm>() |
|
||||
} |
|
||||
@ -1,5 +0,0 @@ |
|||||
import { Snippet } from "." |
|
||||
|
|
||||
export function useSnippet() { |
|
||||
return Snippet.getInstance() |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
import { BaseSingleton } from "base" |
|
||||
import { ApiFactory } from "common/lib/abstract" |
|
||||
|
|
||||
class Snippet extends BaseSingleton { |
|
||||
constructor() { |
|
||||
super() |
|
||||
} |
|
||||
|
|
||||
private get api() { |
|
||||
return ApiFactory.getApiClient() |
|
||||
} |
|
||||
|
|
||||
getTree = async () => { |
|
||||
return this.api.call("SnippetCommand.getTree") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export { Snippet } |
|
||||
@ -1,16 +0,0 @@ |
|||||
import { EventMaps } from "helper/updater/common" |
|
||||
import { defineStore } from "pinia" |
|
||||
|
|
||||
export const useSettingStore = defineStore( |
|
||||
"Updater", |
|
||||
() => { |
|
||||
getApi<EventMaps>().on("update-progress", (_, data) => { |
|
||||
console.log(data) |
|
||||
}) |
|
||||
|
|
||||
return {} |
|
||||
}, |
|
||||
{ |
|
||||
persist: false, |
|
||||
}, |
|
||||
) |
|
||||
@ -1,7 +0,0 @@ |
|||||
## event |
|
||||
|
|
||||
通用事件处理模块 |
|
||||
|
|
||||
- main/**/* 处理主进程的模块 |
|
||||
- main/command.ts 会通过ioc收集,进入依赖管理中 |
|
||||
- 其他 处理渲染进程的模块 |
|
||||
@ -1,165 +0,0 @@ |
|||||
import { inject, injectable } from "inversify" |
|
||||
// import Setting from "./modules/setting"
|
|
||||
// import DB from "./modules/db"
|
|
||||
import Api from "./modules/api" |
|
||||
import WindowManager from "./modules/window-manager" |
|
||||
import { app, nativeTheme, protocol, WebContentsView } from "electron" |
|
||||
import { electronApp } from "@electron-toolkit/utils" |
|
||||
// import Tabs from "./modules/tabs/Tabs"
|
|
||||
import { getFileUrl } from "./utils" |
|
||||
import BaseClass from "./base/base" |
|
||||
|
|
||||
protocol.registerSchemesAsPrivileged([ |
|
||||
// {
|
|
||||
// scheme: "http",
|
|
||||
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
|
|
||||
// },
|
|
||||
// {
|
|
||||
// scheme: "https",
|
|
||||
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
|
|
||||
// },
|
|
||||
// { scheme: "mailto", privileges: { standard: true } },
|
|
||||
{ |
|
||||
scheme: "api", |
|
||||
privileges: { |
|
||||
standard: true, |
|
||||
secure: true, |
|
||||
supportFetchAPI: true, |
|
||||
}, |
|
||||
}, |
|
||||
]) |
|
||||
|
|
||||
@injectable() |
|
||||
class App extends BaseClass { |
|
||||
destroy() { |
|
||||
// destroyAll()
|
|
||||
// 这里是应用正常退出
|
|
||||
} |
|
||||
// private _setting: Setting
|
|
||||
// private _db: DB
|
|
||||
private _Api: Api |
|
||||
private _windowManager: WindowManager |
|
||||
// private _tabs: Tabs
|
|
||||
|
|
||||
constructor( |
|
||||
// @inject(Setting) setting: Setting,
|
|
||||
// @inject(DB) db: DB,
|
|
||||
@inject(Api) Api: Api, |
|
||||
@inject(WindowManager) windowManager: WindowManager, |
|
||||
// @inject(Tabs) tabs: Tabs,
|
|
||||
) { |
|
||||
super() |
|
||||
// this._setting = setting
|
|
||||
// this._db = db
|
|
||||
this._Api = Api |
|
||||
this._windowManager = windowManager |
|
||||
// this._tabs = tabs
|
|
||||
} |
|
||||
|
|
||||
async init() { |
|
||||
this._windowManager.init() |
|
||||
app.whenReady().then(() => { |
|
||||
electronApp.setAppUserModelId("top.xieyaxin") |
|
||||
this.create() |
|
||||
this._Api.init() |
|
||||
}) |
|
||||
app.on("window-all-closed", () => { |
|
||||
if (process.platform !== "darwin") { |
|
||||
app.quit() |
|
||||
} |
|
||||
}) |
|
||||
app.on("will-quit", () => { |
|
||||
this.destroy() |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
create() { |
|
||||
this._windowManager.showMainWindow() |
|
||||
const mainWindow = this._windowManager.getMainWindow() |
|
||||
if (mainWindow) { |
|
||||
nativeTheme.themeSource = "light" |
|
||||
mainWindow.setTitleBarOverlay({ |
|
||||
height: 29, // the smallest size of the title bar on windows accounting for the border on windows 11
|
|
||||
color: "#F8F8F8", |
|
||||
symbolColor: "#000000", |
|
||||
}) |
|
||||
this._windowManager.showWindow("main-top") |
|
||||
const mainTopWindow = this._windowManager.get("main-top") |
|
||||
setTimeout(() => { |
|
||||
// console.log(mainWindow.getParentWindow());
|
|
||||
setTimeout(() => { |
|
||||
mainWindow.contentView.children.length = 0 |
|
||||
const view = new WebContentsView() |
|
||||
view.addChildView(mainTopWindow!.contentView) |
|
||||
view.webContents.loadURL(getFileUrl("about.html")) |
|
||||
// mainTopWindow!.contentView.setBounds({ x: 0, y: 0, width: 100, height: 30 })
|
|
||||
// view.setBounds({ x: 0, y: 0, width: 100, height: 30 })
|
|
||||
mainWindow.contentView.addChildView(view) |
|
||||
// mainWindow.contentView.children.sort()
|
|
||||
console.log(mainWindow.contentView.children) |
|
||||
}, 5000) |
|
||||
// mainWindow.webContents = mainTopWindow!.webContents
|
|
||||
mainWindow.reload() |
|
||||
console.log(mainWindow.webContents.getURL()) |
|
||||
|
|
||||
// mainTopWindow?.destroy()
|
|
||||
// mainWindow.contentView.addChildView(mainWindow.contentView)
|
|
||||
console.log(`child count: `, mainWindow.contentView.children.length) |
|
||||
}, 2000) |
|
||||
// if (mainTopWindow) {
|
|
||||
// mainTopWindow.setParentWindow(mainWindow)
|
|
||||
// mainTopWindow.setIgnoreMouseEvents(true, { forward: false })
|
|
||||
// const listenMove = () => {
|
|
||||
// if (mainWindow && mainTopWindow) {
|
|
||||
// const pos = mainWindow.getPosition()
|
|
||||
// mainTopWindow.setPosition(pos[0], pos[1])
|
|
||||
// }
|
|
||||
// }
|
|
||||
// mainWindow?.on("move", listenMove)
|
|
||||
// const listenResize = () => {
|
|
||||
// if (mainWindow && mainTopWindow) {
|
|
||||
// const size = mainWindow.getSize()
|
|
||||
// console.log(size)
|
|
||||
// mainTopWindow.setSize(size[0], size[1])
|
|
||||
// const pos = mainWindow.getPosition()
|
|
||||
// mainTopWindow.setPosition(pos[0], pos[1])
|
|
||||
// }
|
|
||||
// }
|
|
||||
// listenResize()
|
|
||||
// mainWindow?.on("resize", listenResize)
|
|
||||
// }
|
|
||||
} |
|
||||
// 考虑双browserwindow模式
|
|
||||
/** |
|
||||
* 因为browserwindow可以设置穿透,考虑将tab放在底层window上,其他组件放在上层window上。 |
|
||||
*/ |
|
||||
// const webContentsView = new WebContentsView({
|
|
||||
// webPreferences: {
|
|
||||
// preload: join(__dirname, "../preload/index.mjs"),
|
|
||||
// transparent: true,
|
|
||||
// nodeIntegration: true,
|
|
||||
// spellcheck: false,
|
|
||||
// contextIsolation: true,
|
|
||||
// },
|
|
||||
// })
|
|
||||
// // mainWindow!.contentView = webContentsView
|
|
||||
// // setTimeout(() => {
|
|
||||
// mainWindow!.contentView.addChildView(webContentsView)
|
|
||||
// // mainWindow?.setIgnoreMouseEvents(true, { forward: true })
|
|
||||
// // }, 2000);
|
|
||||
// webContentsView.webContents.loadURL(getFileUrl("index.html"))
|
|
||||
// const listenResize = () => {
|
|
||||
// const size = mainWindow!.getSize()
|
|
||||
// webContentsView.setBounds({ x: 0, y: 0, width: size[0], height: size[1] })
|
|
||||
// }
|
|
||||
// listenResize()
|
|
||||
// mainWindow!.addListener("resize", listenResize)
|
|
||||
|
|
||||
// this._tabs.add("https://baidu.com", true)
|
|
||||
// this._tabs.add("https://zhihu.com")
|
|
||||
return mainWindow |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default App |
|
||||
export { App } |
|
||||
@ -1,8 +1,12 @@ |
|||||
import Setting, { IConfig } from "setting/main" |
import Setting, { IConfig } from "setting/main" |
||||
|
import { broadcast } from "utils/main" |
||||
|
|
||||
export default class SettingCommand { |
export default class SettingCommand { |
||||
static init() { |
static init() { |
||||
console.log("SettingCommand init") |
Setting.events.on("change", (k, value) => { |
||||
|
console.log(k, value) |
||||
|
broadcast("SettingCommand.change", k, value) |
||||
|
}) |
||||
} |
} |
||||
sync() { |
sync() { |
||||
return Setting.config() |
return Setting.config() |
||||
@ -0,0 +1,8 @@ |
|||||
|
import type { EventMaps } from "setting/common" |
||||
|
|
||||
|
export interface SettingCommand { |
||||
|
save: () => void |
||||
|
reset: () => void |
||||
|
} |
||||
|
|
||||
|
export { EventMaps } |
||||
@ -0,0 +1,66 @@ |
|||||
|
import { inject } from "inversify" |
||||
|
import Tabs from "main/modules/tabs" |
||||
|
import WindowManager from "main/modules/window-manager" |
||||
|
import { broadcast } from "utils/main" |
||||
|
|
||||
|
class TabsCommand { |
||||
|
constructor( |
||||
|
@inject(Tabs) private _Tabs: Tabs, |
||||
|
@inject(WindowManager) private _WindowManager: WindowManager, |
||||
|
) { |
||||
|
this.listenerTabActive = this.listenerTabActive.bind(this) |
||||
|
this._Tabs.events.on("update", this.listenerTabActive) |
||||
|
} |
||||
|
|
||||
|
bindElement(rect) { |
||||
|
this._Tabs.updateRect(rect) |
||||
|
} |
||||
|
|
||||
|
reload() { |
||||
|
this._WindowManager.getMainWindow()?.reload() |
||||
|
} |
||||
|
|
||||
|
sync() { |
||||
|
this.listenerTabActive() |
||||
|
if (!this.getAllTabs().length) { |
||||
|
this.add("about:blank") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
listenerTabActive() { |
||||
|
broadcast("TabsCommand.update", this.getAllTabs()) |
||||
|
} |
||||
|
|
||||
|
add(url) { |
||||
|
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
||||
|
} |
||||
|
|
||||
|
nagivate(index: number, url: string) { |
||||
|
this._Tabs.navigate(+index, url) |
||||
|
} |
||||
|
|
||||
|
setActive(index) { |
||||
|
this._Tabs.changeActive(index) |
||||
|
} |
||||
|
|
||||
|
closeTab(e) { |
||||
|
this._Tabs.remove(e.body.active) |
||||
|
} |
||||
|
|
||||
|
closeAll() { |
||||
|
this._Tabs.closeAll() |
||||
|
} |
||||
|
|
||||
|
getAllTabs() { |
||||
|
return this._Tabs._tabs.map(v => ({ |
||||
|
url: v.url, |
||||
|
showUrl: v.showUrl, |
||||
|
title: v.title, |
||||
|
favicons: v.favicons, |
||||
|
isActive: v.isActive, |
||||
|
})) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { TabsCommand } |
||||
|
export default TabsCommand |
||||
@ -0,0 +1,13 @@ |
|||||
|
.slide-fade-enter-active { |
||||
|
transition: all 0.2s ease-out; |
||||
|
} |
||||
|
|
||||
|
.slide-fade-leave-active { |
||||
|
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); |
||||
|
} |
||||
|
|
||||
|
.slide-fade-enter-from, |
||||
|
.slide-fade-leave-to { |
||||
|
// transform: translateX(20px); |
||||
|
opacity: 0; |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
@use "bulma/sass/base"; |
||||
|
@use "bulma/sass/themes"; |
||||
|
|
||||
|
@use "bulma/sass/elements/button"; |
||||
|
@use "bulma/sass/form/input-textarea"; |
||||
@ -1,4 +1,4 @@ |
|||||
import { ApiFactory } from "common/lib/abstract" |
import { ApiFactory } from "base/api/abstract" |
||||
import { BaseSingleton } from "base" |
import { BaseSingleton } from "base" |
||||
import { IConfig } from "config" |
import { IConfig } from "config" |
||||
|
|
||||
@ -1,4 +1,4 @@ |
|||||
import { BaseEvent } from "common/lib/abstract" |
import { BaseEvent } from "base/api/abstract" |
||||
|
|
||||
class Updater extends BaseEvent { |
class Updater extends BaseEvent { |
||||
constructor() { |
constructor() { |
||||
@ -0,0 +1,10 @@ |
|||||
|
<template> |
||||
|
<div class="w-[calc((100%-4*20px)/5)] <lg:w-[calc((100%-2*20px)/3)]" shadow> |
||||
|
<div p-2 text-lg font-bold>MarkdownUtils</div> |
||||
|
<div p-2 pt-0 text-sm>这是一个导航站</div> |
||||
|
<div flex gap="10px" px-4 py-2 tex border-t="1px solid #E5E5E5"> |
||||
|
<div cursor="pointer" text-sm leading-1 py-2 px-3 border="1px solid #E5E5E5" rounded @click="$router.push('browser')">查看</div> |
||||
|
<div cursor="pointer" text-sm leading-1 py-2 px-3 border="1px solid #E5E5E5" rounded>访问</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,105 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useMonacoEditor, IOptions } from "./hook" |
||||
|
|
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
readonly?: boolean |
||||
|
modelValue?: string |
||||
|
filename?: string |
||||
|
placeholder?: (() => Node) | string |
||||
|
modelOptions?: IOptions["modelOptions"] |
||||
|
editorOptions?: IOptions["editorOptions"] |
||||
|
}>(), |
||||
|
{ |
||||
|
readonly: false, |
||||
|
modelValue: "", |
||||
|
filename: "", |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
(e: "update:modelValue", code: string): void |
||||
|
(e: "change", code: string): void |
||||
|
(e: "cursor:position", position: [number, number]): void |
||||
|
}>() |
||||
|
|
||||
|
const editorRef = ref<HTMLDivElement>() |
||||
|
const { updateOption, setValue } = useMonacoEditor(editorRef, { |
||||
|
placeholder: props.placeholder, |
||||
|
content: props.modelValue, |
||||
|
filename: props.filename, |
||||
|
modelOptions: props.modelOptions, |
||||
|
editorOptions: props.editorOptions, |
||||
|
onCursorChange(e) { |
||||
|
emit("cursor:position", [e.position.lineNumber, e.position.column]) |
||||
|
}, |
||||
|
onDidChangeContent(code) { |
||||
|
emit("update:modelValue", code) |
||||
|
emit("change", code) |
||||
|
}, |
||||
|
}) |
||||
|
watch( |
||||
|
() => props.modelValue, |
||||
|
() => { |
||||
|
setValue(props.modelValue) |
||||
|
}, |
||||
|
) |
||||
|
watch( |
||||
|
() => props.filename, |
||||
|
() => { |
||||
|
updateOption({ |
||||
|
filename: props.filename, |
||||
|
}) |
||||
|
}, |
||||
|
) |
||||
|
watch( |
||||
|
() => props.editorOptions, |
||||
|
() => { |
||||
|
updateOption({ |
||||
|
editorOptions: props.editorOptions, |
||||
|
}) |
||||
|
}, |
||||
|
{ deep: true }, |
||||
|
) |
||||
|
watch( |
||||
|
() => props.modelOptions, |
||||
|
() => { |
||||
|
updateOption({ |
||||
|
modelOptions: props.modelOptions, |
||||
|
}) |
||||
|
}, |
||||
|
{ deep: true }, |
||||
|
) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="monaco-wrapper"> |
||||
|
<div ref="editorRef" class="monaco-editor"></div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.monaco-wrapper { |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
|
||||
|
.monaco-editor { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.monaco-bg { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
pointer-events: none; |
||||
|
opacity: 0.1; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.monaco-logo { |
||||
|
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,209 @@ |
|||||
|
import { monaco } from "./monaco" |
||||
|
import { PlaceholderContentWidget } from "./PlaceholderContentWidget" |
||||
|
import { judgeFile } from "./utils" |
||||
|
import type { Ref } from "vue" |
||||
|
|
||||
|
function useResizeObserver(el: HTMLDivElement, callback: ResizeObserverCallback) { |
||||
|
const isSupported = window && "ResizeObserver" in window |
||||
|
let observer: ResizeObserver | undefined |
||||
|
const cleanup = () => { |
||||
|
if (observer) { |
||||
|
observer.disconnect() |
||||
|
observer = undefined |
||||
|
} |
||||
|
} |
||||
|
const stopWatch = watch( |
||||
|
() => el, |
||||
|
el => { |
||||
|
cleanup() |
||||
|
if (isSupported && window && el) { |
||||
|
observer = new ResizeObserver(callback) |
||||
|
observer!.observe(el, {}) |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true }, |
||||
|
) |
||||
|
const stop = () => { |
||||
|
cleanup() |
||||
|
stopWatch() |
||||
|
} |
||||
|
function tryOnScopeDispose(fn: () => void) { |
||||
|
if (getCurrentScope()) { |
||||
|
onScopeDispose(fn) |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
tryOnScopeDispose(() => { |
||||
|
stop() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export interface IOptions { |
||||
|
placeholder: (() => Node) | string | undefined |
||||
|
filename: string |
||||
|
content: string |
||||
|
editorOptions: monaco.editor.IEditorOptions & monaco.editor.IGlobalEditorOptions |
||||
|
modelOptions: monaco.editor.ITextModelUpdateOptions |
||||
|
onCursorChange?: (e: monaco.editor.ICursorPositionChangedEvent) => void |
||||
|
onDidChangeContent?: (str: string) => void |
||||
|
} |
||||
|
|
||||
|
const defaultOptions: IOptions = { |
||||
|
placeholder: undefined, |
||||
|
filename: "temp", |
||||
|
content: "", |
||||
|
editorOptions: { |
||||
|
fontSize: 14, |
||||
|
readOnly: false, |
||||
|
theme: "vs-light", |
||||
|
fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", |
||||
|
scrollBeyondLastLine: false, |
||||
|
lineHeight: 22, |
||||
|
automaticLayout: true, |
||||
|
minimap: { |
||||
|
enabled: false, |
||||
|
}, |
||||
|
}, |
||||
|
modelOptions: {}, |
||||
|
} |
||||
|
|
||||
|
const assign = (curOpt, opt, parenyKey: string[] = [], config = { arrayExtend: "concat" }) => { |
||||
|
for (const key in opt) { |
||||
|
if (opt[key] !== undefined) { |
||||
|
if (typeof opt[key] === "function" && curOpt[key] !== undefined && typeof curOpt[key] !== "function") { |
||||
|
opt[key] = opt[key](curOpt[key]) |
||||
|
} |
||||
|
if (typeof opt[key] === "object" && !Array.isArray(opt[key]) && !Array.isArray(curOpt[key])) { |
||||
|
parenyKey.push(key) |
||||
|
assign(curOpt[key], opt[key], parenyKey, config) |
||||
|
} else if (typeof opt[key] === "object" && Array.isArray(opt[key]) && Array.isArray(curOpt[key])) { |
||||
|
if (config.arrayExtend === "concat") { |
||||
|
curOpt[key] = curOpt[key].concat(opt[key]) |
||||
|
} else { |
||||
|
curOpt[key] = opt[key] |
||||
|
} |
||||
|
} else if (curOpt[key] !== undefined && typeof curOpt[key] !== typeof opt[key]) { |
||||
|
throw new Error(`Type of ${parenyKey.join(",") + "." + key} is not match`) |
||||
|
} else { |
||||
|
curOpt[key] = opt[key] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return curOpt |
||||
|
} |
||||
|
|
||||
|
function getOptions(opt = {}, config = { arrayExtend: "concat" }): IOptions { |
||||
|
const curOptions = structuredClone(defaultOptions) |
||||
|
assign(curOptions, opt, [], config) |
||||
|
return curOptions |
||||
|
} |
||||
|
|
||||
|
export function useMonacoEditor(editorElement: Ref<HTMLDivElement | undefined>, opts: Partial<IOptions>) { |
||||
|
let curOption = getOptions(opts) as IOptions |
||||
|
let editor: monaco.editor.IStandaloneCodeEditor | null = null |
||||
|
let placeholderWidget: PlaceholderContentWidget | null = null |
||||
|
|
||||
|
const updateOption = (opts: Partial<IOptions>) => { |
||||
|
if (!editor) return |
||||
|
curOption = assign(curOption, opts) |
||||
|
if (Object.hasOwn(opts, "placeholder")) { |
||||
|
if (placeholderWidget) { |
||||
|
placeholderWidget.dispose() |
||||
|
placeholderWidget = null |
||||
|
} |
||||
|
if (opts.placeholder) { |
||||
|
placeholderWidget = new PlaceholderContentWidget(opts.placeholder, editor) |
||||
|
} |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "modelOptions") && opts.modelOptions) { |
||||
|
const model = editor.getModel() |
||||
|
model?.updateOptions(opts.modelOptions) |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "editorOptions") && opts.editorOptions) { |
||||
|
editor.updateOptions(opts.editorOptions) |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "filename")) { |
||||
|
updateModel(curOption.filename, curOption.content) |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "content")) { |
||||
|
console.log("无法通过updateOption修改content") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let isInnerChange = "waitting" // waitting, out, in
|
||||
|
const setValue = (content: string) => { |
||||
|
if (isInnerChange === "waitting") { |
||||
|
isInnerChange = "out" |
||||
|
} |
||||
|
if (editor && isInnerChange === "out") { |
||||
|
editor.setValue(content) |
||||
|
} else { |
||||
|
isInnerChange = "waitting" |
||||
|
} |
||||
|
} |
||||
|
function updateModel(name: string, content: string) { |
||||
|
if (editor) { |
||||
|
const oldModel = editor.getModel() //获取旧模型
|
||||
|
const file = judgeFile(name) |
||||
|
// 这样定义的话model无法清除
|
||||
|
// monaco.editor.createModel("const a = 111","typescript", monaco.Uri.parse('file://root/file3.ts'))
|
||||
|
const model: monaco.editor.ITextModel = monaco.editor.createModel(content ?? "", file?.language ?? "txt") |
||||
|
model.updateOptions(curOption.modelOptions) |
||||
|
model.onDidChangeContent(() => { |
||||
|
if (model) { |
||||
|
if (isInnerChange === "out") { |
||||
|
isInnerChange = "waitting" |
||||
|
return |
||||
|
} |
||||
|
isInnerChange = "in" |
||||
|
const code = model.getValue() |
||||
|
curOption.onDidChangeContent?.(code) |
||||
|
} |
||||
|
}) |
||||
|
if (oldModel) { |
||||
|
oldModel.dispose() |
||||
|
} |
||||
|
editor.setModel(model) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
tryOnMounted(() => { |
||||
|
if (editorElement.value && !editor) { |
||||
|
editor = monaco.editor.create(editorElement.value, curOption.editorOptions) as monaco.editor.IStandaloneCodeEditor |
||||
|
editor.onDidChangeCursorPosition(e => { |
||||
|
curOption.onCursorChange?.(e) |
||||
|
}) |
||||
|
if (!curOption.content) { |
||||
|
placeholderWidget = new PlaceholderContentWidget(curOption.placeholder || "", editor) |
||||
|
} else { |
||||
|
if (isInnerChange === "waitting") { |
||||
|
isInnerChange = "out" |
||||
|
} |
||||
|
} |
||||
|
updateModel(curOption.filename, curOption.content) |
||||
|
useResizeObserver(editorElement.value, () => { |
||||
|
editor!.layout() |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
tryOnUnmounted(() => { |
||||
|
if (editor) { |
||||
|
const oldModel = editor.getModel() |
||||
|
if (oldModel) { |
||||
|
oldModel.dispose() |
||||
|
} |
||||
|
editor.dispose() |
||||
|
editor = null |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return { |
||||
|
setValue, |
||||
|
scrollTop() { |
||||
|
editor?.setScrollTop(0) |
||||
|
}, |
||||
|
updateOption, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="w-full h-full bg-[#E9ECEF] grid place-content-center place-items-center"> |
||||
|
<div shadow="lg" bg="light-100" |
||||
|
style="width: 375px; height: 667px; transform: scale(1); transform-origin: center center;"> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,59 @@ |
|||||
|
import { monaco } from "./monaco" |
||||
|
|
||||
|
/** |
||||
|
* Represents a placeholder renderer for monaco editor |
||||
|
* Roughly based on https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts
|
||||
|
*/ |
||||
|
export class PlaceholderContentWidget implements monaco.editor.IContentWidget { |
||||
|
private static readonly ID = "editor.widget.placeholderHint" |
||||
|
|
||||
|
private domNode: HTMLElement | undefined |
||||
|
|
||||
|
constructor( |
||||
|
private readonly placeholder: (() => Node) | string, |
||||
|
private readonly editor: monaco.editor.ICodeEditor, |
||||
|
) { |
||||
|
// register a listener for editor code changes
|
||||
|
editor.onDidChangeModelContent(() => this.onDidChangeModelContent()) |
||||
|
// ensure that on initial load the placeholder is shown
|
||||
|
this.onDidChangeModelContent() |
||||
|
} |
||||
|
|
||||
|
private onDidChangeModelContent(): void { |
||||
|
if (this.editor.getValue() === "") { |
||||
|
this.editor.addContentWidget(this) |
||||
|
} else { |
||||
|
this.editor.removeContentWidget(this) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getId(): string { |
||||
|
return PlaceholderContentWidget.ID |
||||
|
} |
||||
|
|
||||
|
getDomNode(): HTMLElement { |
||||
|
if (!this.domNode) { |
||||
|
this.domNode = document.createElement("div") |
||||
|
this.domNode.style.width = "max-content" |
||||
|
this.domNode.style.pointerEvents = "none" // 整个容器禁用指针事件
|
||||
|
this.domNode.style.fontStyle = "italic" |
||||
|
this.domNode.style.opacity = "0.6" // 添加透明度,更像 placeholder
|
||||
|
|
||||
|
this.domNode.appendChild(typeof this.placeholder === "function" ? this.placeholder() : document.createTextNode(this.placeholder)) |
||||
|
this.editor.applyFontInfo(this.domNode) |
||||
|
} |
||||
|
|
||||
|
return this.domNode |
||||
|
} |
||||
|
|
||||
|
getPosition(): monaco.editor.IContentWidgetPosition | null { |
||||
|
return { |
||||
|
position: { lineNumber: 1, column: 1 }, |
||||
|
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT], |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dispose(): void { |
||||
|
this.editor.removeContentWidget(this) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,211 @@ |
|||||
|
import { monaco } from "./monaco" |
||||
|
import { PlaceholderContentWidget } from "./PlaceholderContentWidget" |
||||
|
import { judgeFile } from "./utils" |
||||
|
import type { Ref } from "vue" |
||||
|
|
||||
|
function useResizeObserver(el: HTMLDivElement, callback: ResizeObserverCallback) { |
||||
|
const isSupported = window && "ResizeObserver" in window |
||||
|
let observer: ResizeObserver | undefined |
||||
|
const cleanup = () => { |
||||
|
if (observer) { |
||||
|
observer.disconnect() |
||||
|
observer = undefined |
||||
|
} |
||||
|
} |
||||
|
const stopWatch = watch( |
||||
|
() => el, |
||||
|
el => { |
||||
|
cleanup() |
||||
|
if (isSupported && window && el) { |
||||
|
observer = new ResizeObserver(callback) |
||||
|
observer!.observe(el, {}) |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true }, |
||||
|
) |
||||
|
const stop = () => { |
||||
|
cleanup() |
||||
|
stopWatch() |
||||
|
} |
||||
|
function tryOnScopeDispose(fn: () => void) { |
||||
|
if (getCurrentScope()) { |
||||
|
onScopeDispose(fn) |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
tryOnScopeDispose(() => { |
||||
|
stop() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export interface IOptions { |
||||
|
placeholder: (() => Node) | string | undefined |
||||
|
filename: string |
||||
|
extendsExt?: {language: string, ext?: string, pre?: string}[] |
||||
|
content: string |
||||
|
editorOptions: monaco.editor.IEditorOptions & monaco.editor.IGlobalEditorOptions |
||||
|
modelOptions: monaco.editor.ITextModelUpdateOptions |
||||
|
onCursorChange?: (e: monaco.editor.ICursorPositionChangedEvent) => void |
||||
|
onDidChangeContent?: (str: string) => void |
||||
|
} |
||||
|
|
||||
|
const defaultOptions: IOptions = { |
||||
|
placeholder: undefined, |
||||
|
filename: "temp", |
||||
|
extendsExt: [], |
||||
|
content: "", |
||||
|
editorOptions: { |
||||
|
fontSize: 14, |
||||
|
readOnly: false, |
||||
|
theme: "vs-light", |
||||
|
fontFamily: "Cascadia Mono, Consolas, 'Courier New', monospace", |
||||
|
scrollBeyondLastLine: false, |
||||
|
lineHeight: 22, |
||||
|
automaticLayout: true, |
||||
|
minimap: { |
||||
|
enabled: false, |
||||
|
}, |
||||
|
}, |
||||
|
modelOptions: {}, |
||||
|
} |
||||
|
|
||||
|
const assign = (curOpt, opt, parenyKey: string[] = [], config = { arrayExtend: "concat" }) => { |
||||
|
for (const key in opt) { |
||||
|
if (opt[key] !== undefined) { |
||||
|
if (typeof opt[key] === "function" && curOpt[key] !== undefined && typeof curOpt[key] !== "function") { |
||||
|
opt[key] = opt[key](curOpt[key]) |
||||
|
} |
||||
|
if (typeof opt[key] === "object" && !Array.isArray(opt[key]) && !Array.isArray(curOpt[key])) { |
||||
|
parenyKey.push(key) |
||||
|
assign(curOpt[key], opt[key], parenyKey, config) |
||||
|
} else if (typeof opt[key] === "object" && Array.isArray(opt[key]) && Array.isArray(curOpt[key])) { |
||||
|
if (config.arrayExtend === "concat") { |
||||
|
curOpt[key] = curOpt[key].concat(opt[key]) |
||||
|
} else { |
||||
|
curOpt[key] = opt[key] |
||||
|
} |
||||
|
} else if (curOpt[key] !== undefined && typeof curOpt[key] !== typeof opt[key]) { |
||||
|
throw new Error(`Type of ${parenyKey.join(",") + "." + key} is not match`) |
||||
|
} else { |
||||
|
curOpt[key] = opt[key] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return curOpt |
||||
|
} |
||||
|
|
||||
|
function getOptions(opt = {}, config = { arrayExtend: "concat" }): IOptions { |
||||
|
const curOptions = structuredClone(defaultOptions) |
||||
|
assign(curOptions, opt, [], config) |
||||
|
return curOptions |
||||
|
} |
||||
|
|
||||
|
export function useMonacoEditor(editorElement: Ref<HTMLDivElement | undefined>, opts: Partial<IOptions>) { |
||||
|
let curOption = getOptions(opts) as IOptions |
||||
|
let editor: monaco.editor.IStandaloneCodeEditor | null = null |
||||
|
let placeholderWidget: PlaceholderContentWidget | null = null |
||||
|
|
||||
|
const updateOption = (opts: Partial<IOptions>) => { |
||||
|
if (!editor) return |
||||
|
curOption = assign(curOption, opts) |
||||
|
if (Object.hasOwn(opts, "placeholder")) { |
||||
|
if (placeholderWidget) { |
||||
|
placeholderWidget.dispose() |
||||
|
placeholderWidget = null |
||||
|
} |
||||
|
if (opts.placeholder) { |
||||
|
placeholderWidget = new PlaceholderContentWidget(opts.placeholder, editor) |
||||
|
} |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "modelOptions") && opts.modelOptions) { |
||||
|
const model = editor.getModel() |
||||
|
model?.updateOptions(opts.modelOptions) |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "editorOptions") && opts.editorOptions) { |
||||
|
editor.updateOptions(opts.editorOptions) |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "filename")) { |
||||
|
updateModel(curOption.filename, curOption.content) |
||||
|
} |
||||
|
if (Object.hasOwn(opts, "content")) { |
||||
|
console.log("无法通过updateOption修改content") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let isInnerChange = "waitting" // waitting, out, in
|
||||
|
const setValue = (content: string) => { |
||||
|
if (isInnerChange === "waitting") { |
||||
|
isInnerChange = "out" |
||||
|
} |
||||
|
if (editor && isInnerChange === "out") { |
||||
|
editor.setValue(content) |
||||
|
} else { |
||||
|
isInnerChange = "waitting" |
||||
|
} |
||||
|
} |
||||
|
function updateModel(name: string, content: string) { |
||||
|
if (editor) { |
||||
|
const oldModel = editor.getModel() //获取旧模型
|
||||
|
const file = judgeFile(name, curOption.extendsExt || []) |
||||
|
// 这样定义的话model无法清除
|
||||
|
// monaco.editor.createModel("const a = 111","typescript", monaco.Uri.parse('file://root/file3.ts'))
|
||||
|
const model: monaco.editor.ITextModel = monaco.editor.createModel(content ?? "", file?.language ?? "txt") |
||||
|
model.updateOptions(curOption.modelOptions) |
||||
|
model.onDidChangeContent(() => { |
||||
|
if (model) { |
||||
|
if (isInnerChange === "out") { |
||||
|
isInnerChange = "waitting" |
||||
|
return |
||||
|
} |
||||
|
isInnerChange = "in" |
||||
|
const code = model.getValue() |
||||
|
curOption.onDidChangeContent?.(code) |
||||
|
} |
||||
|
}) |
||||
|
if (oldModel) { |
||||
|
oldModel.dispose() |
||||
|
} |
||||
|
editor.setModel(model) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
tryOnMounted(() => { |
||||
|
if (editorElement.value && !editor) { |
||||
|
editor = monaco.editor.create(editorElement.value, curOption.editorOptions) as monaco.editor.IStandaloneCodeEditor |
||||
|
editor.onDidChangeCursorPosition(e => { |
||||
|
curOption.onCursorChange?.(e) |
||||
|
}) |
||||
|
if (!curOption.content) { |
||||
|
placeholderWidget = new PlaceholderContentWidget(curOption.placeholder || "", editor) |
||||
|
} else { |
||||
|
if (isInnerChange === "waitting") { |
||||
|
isInnerChange = "out" |
||||
|
} |
||||
|
} |
||||
|
updateModel(curOption.filename, curOption.content) |
||||
|
useResizeObserver(editorElement.value, () => { |
||||
|
editor!.layout() |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
tryOnUnmounted(() => { |
||||
|
if (editor) { |
||||
|
const oldModel = editor.getModel() |
||||
|
if (oldModel) { |
||||
|
oldModel.dispose() |
||||
|
} |
||||
|
editor.dispose() |
||||
|
editor = null |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return { |
||||
|
setValue, |
||||
|
scrollTop() { |
||||
|
editor?.setScrollTop(0) |
||||
|
}, |
||||
|
updateOption, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,107 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { useMonacoEditor, IOptions } from "./hook" |
||||
|
|
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
readonly?: boolean |
||||
|
modelValue?: string |
||||
|
filename?: string |
||||
|
placeholder?: (() => Node) | string |
||||
|
extendsExt?: {language: string, ext?: string, pre?: string}[] |
||||
|
modelOptions?: IOptions["modelOptions"] |
||||
|
editorOptions?: IOptions["editorOptions"] |
||||
|
}>(), |
||||
|
{ |
||||
|
readonly: false, |
||||
|
modelValue: "", |
||||
|
filename: "", |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
(e: "update:modelValue", code: string): void |
||||
|
(e: "change", code: string): void |
||||
|
(e: "cursor:position", position: [number, number]): void |
||||
|
}>() |
||||
|
|
||||
|
const editorRef = ref<HTMLDivElement>() |
||||
|
const { updateOption, setValue } = useMonacoEditor(editorRef, { |
||||
|
placeholder: props.placeholder, |
||||
|
content: props.modelValue, |
||||
|
filename: props.filename, |
||||
|
extendsExt: props.extendsExt, |
||||
|
modelOptions: props.modelOptions, |
||||
|
editorOptions: props.editorOptions, |
||||
|
onCursorChange(e) { |
||||
|
emit("cursor:position", [e.position.lineNumber, e.position.column]) |
||||
|
}, |
||||
|
onDidChangeContent(code) { |
||||
|
emit("update:modelValue", code) |
||||
|
emit("change", code) |
||||
|
}, |
||||
|
}) |
||||
|
watch( |
||||
|
() => props.modelValue, |
||||
|
() => { |
||||
|
setValue(props.modelValue) |
||||
|
}, |
||||
|
) |
||||
|
watch( |
||||
|
() => props.filename, |
||||
|
() => { |
||||
|
updateOption({ |
||||
|
filename: props.filename, |
||||
|
}) |
||||
|
}, |
||||
|
) |
||||
|
watch( |
||||
|
() => props.editorOptions, |
||||
|
() => { |
||||
|
updateOption({ |
||||
|
editorOptions: props.editorOptions, |
||||
|
}) |
||||
|
}, |
||||
|
{ deep: true }, |
||||
|
) |
||||
|
watch( |
||||
|
() => props.modelOptions, |
||||
|
() => { |
||||
|
updateOption({ |
||||
|
modelOptions: props.modelOptions, |
||||
|
}) |
||||
|
}, |
||||
|
{ deep: true }, |
||||
|
) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="monaco-wrapper"> |
||||
|
<div ref="editorRef" class="monaco-editor"></div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.monaco-wrapper { |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
|
||||
|
.monaco-editor { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.monaco-bg { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
pointer-events: none; |
||||
|
opacity: 0.1; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.monaco-logo { |
||||
|
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,19 @@ |
|||||
|
// import 'monaco-editor/esm/vs/editor/editor.all.js';
|
||||
|
|
||||
|
// import 'monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.js';
|
||||
|
|
||||
|
// import 'monaco-editor/esm/vs/basic-languages/monaco.contribution.js';
|
||||
|
|
||||
|
// import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution.js';
|
||||
|
// import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js';
|
||||
|
// import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js';
|
||||
|
// import 'monaco-editor/esm/vs/basic-languages/css/css.contribution.js';
|
||||
|
// import 'monaco-editor/esm/vs/basic-languages/java/java.contribution.js';
|
||||
|
|
||||
|
// 导入全部特性
|
||||
|
import * as monaco from "monaco-editor" |
||||
|
|
||||
|
// import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
|
||||
|
// import "monaco-editor/esm/vs/basic-languages/monaco.contribution.js"
|
||||
|
|
||||
|
export { monaco } |
||||
@ -0,0 +1,33 @@ |
|||||
|
export function judgeFile(filename: string, extendsExt?: {language: string, ext?: string, pre?: string}[]) { |
||||
|
if (!filename) return |
||||
|
const ext = [ |
||||
|
{ language: "vue", ext: ".vue", index: -1 }, |
||||
|
{ language: "javascript", ext: ".js", index: -1 }, |
||||
|
{ language: "css", ext: ".css", index: -1 }, |
||||
|
{ language: "html", ext: ".html", index: -1 }, |
||||
|
{ language: "tsx", ext: ".tsx", index: -1 }, |
||||
|
{ language: "typescript", ext: ".ts", index: -1 }, |
||||
|
{ language: "markdown", ext: ".md", index: -1 }, |
||||
|
{ language: "json", ext: ".json", index: -1 }, |
||||
|
{ language: "web", ext: ".web", index: -1 }, |
||||
|
{ language: "dot", pre: ".", index: -1 }, |
||||
|
...(extendsExt || []).map(e => ({ ...e, index: -1 })), |
||||
|
] |
||||
|
let cur |
||||
|
for (let i = 0; i < ext.length; i++) { |
||||
|
const e = ext[i] |
||||
|
if (e.ext && filename.endsWith(e.ext)) { |
||||
|
const index = filename.lastIndexOf(e.ext) |
||||
|
e.index = index |
||||
|
cur = e |
||||
|
break |
||||
|
} |
||||
|
if (e.pre && filename.startsWith(e.pre)) { |
||||
|
const index = filename.indexOf(e.pre) |
||||
|
e.index = index |
||||
|
cur = e |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
return cur |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
import { LogLevel } from "logger/common" |
||||
|
import { PlatForm } from "@/commands/PlatForm" |
||||
|
|
||||
|
export function useApiPlatForm() { |
||||
|
const plat = PlatForm.getInstance<PlatForm>() |
||||
|
|
||||
|
// 全屏状态
|
||||
|
const isFullScreen = ref(false) |
||||
|
;(async () => { |
||||
|
isFullScreen.value = await plat.isFullScreen() |
||||
|
})() |
||||
|
|
||||
|
const toggleFullScreen = async () => { |
||||
|
await plat.toggleFullScreen() |
||||
|
isFullScreen.value = !isFullScreen.value |
||||
|
} |
||||
|
// 全屏状态 END
|
||||
|
|
||||
|
const curLogLevel = ref<LogLevel>() |
||||
|
;(async () => { |
||||
|
curLogLevel.value = await plat.logGetLevel() |
||||
|
})() |
||||
|
const isOpenDebug = computed(() => curLogLevel.value === LogLevel.TRACE) |
||||
|
const toggleDebugMode = async () => { |
||||
|
if (curLogLevel.value === LogLevel.TRACE) { |
||||
|
await plat.logSetLevel(LogLevel.INFO) |
||||
|
curLogLevel.value = LogLevel.INFO |
||||
|
return |
||||
|
} |
||||
|
await plat.logSetLevel(LogLevel.TRACE) |
||||
|
curLogLevel.value = LogLevel.TRACE |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
power: plat, |
||||
|
isOpenDebug, |
||||
|
toggleDebugMode, |
||||
|
toggleFullScreen, |
||||
|
isFullScreen, |
||||
|
} |
||||
|
} |
||||
@ -1,12 +1,20 @@ |
|||||
import { defineStore } from "pinia" |
import { defineStore } from "pinia" |
||||
import { Setting } from "." |
import { Setting } from "@/commands/Setting" |
||||
import type { IConfig } from "config" |
import type { IConfig } from "config" |
||||
|
import type { EventMaps, SettingCommand } from "command/Setting/type" |
||||
|
|
||||
let rawConfig: IConfig = Setting.getInstance().sync() as unknown as IConfig |
let rawConfig: IConfig = Setting.getInstance().sync() as unknown as IConfig |
||||
|
|
||||
export const useSettingStore = defineStore( |
export const useApiSetting = defineStore( |
||||
"Setting", |
"Setting", |
||||
() => { |
() => { |
||||
|
const api = getApi<SettingCommand, EventMaps, "SettingCommand">() |
||||
|
|
||||
|
api.on("SettingCommand.change", (_, k, v) => { |
||||
|
rawConfig[k] = v |
||||
|
config.value = JSON.parse(JSON.stringify(rawConfig)) |
||||
|
}) |
||||
|
|
||||
const config = ref(JSON.parse(JSON.stringify(rawConfig))) |
const config = ref(JSON.parse(JSON.stringify(rawConfig))) |
||||
const diffKeys = ref<(keyof IConfig)[]>([]) |
const diffKeys = ref<(keyof IConfig)[]>([]) |
||||
const isSame = computed(() => { |
const isSame = computed(() => { |
||||
@ -0,0 +1,70 @@ |
|||||
|
import { EventMaps, UpdaterCommand } from "helper/updater/common" |
||||
|
import { defineStore } from "pinia" |
||||
|
|
||||
|
export const enum ApiUpdaterStatus { |
||||
|
Idle = "idle", |
||||
|
Checking = "checking", |
||||
|
StartChecking = "start-checking", |
||||
|
UpdateAvailable = "update-available", |
||||
|
UpdateNotAvailable = "update-not-available", |
||||
|
Downloading = "downloading", |
||||
|
Error = "error", |
||||
|
} |
||||
|
|
||||
|
export const useApiUpdater = defineStore( |
||||
|
"Updater", |
||||
|
() => { |
||||
|
const curStatus = ref(ApiUpdaterStatus.Idle) |
||||
|
const speed = ref(0) |
||||
|
const percent = ref(0) |
||||
|
const all = ref(0) |
||||
|
const now = ref(0) |
||||
|
|
||||
|
const isNeedUpdate = ref(false) |
||||
|
|
||||
|
const api = getApi<UpdaterCommand, EventMaps, "UpdaterCommand">() |
||||
|
api.on("UpdaterCommand.error", (_, data) => { |
||||
|
curStatus.value = ApiUpdaterStatus.Error |
||||
|
console.log(data) |
||||
|
}) |
||||
|
api.on("UpdaterCommand.update-not-available", () => { |
||||
|
curStatus.value = ApiUpdaterStatus.UpdateNotAvailable |
||||
|
isNeedUpdate.value = false |
||||
|
}) |
||||
|
api.on("UpdaterCommand.update-available", () => { |
||||
|
curStatus.value = ApiUpdaterStatus.UpdateAvailable |
||||
|
isNeedUpdate.value = true |
||||
|
}) |
||||
|
api.on("UpdaterCommand.update-progress", (_, data) => { |
||||
|
curStatus.value = ApiUpdaterStatus.Downloading |
||||
|
speed.value = +(data.speed / 1000).toFixed(2) // Convert to KB/s
|
||||
|
percent.value = data.percent |
||||
|
all.value = data.all |
||||
|
now.value = data.now |
||||
|
isNeedUpdate.value = false |
||||
|
}) |
||||
|
api.on("UpdaterCommand.checking-for-update", () => { |
||||
|
curStatus.value = ApiUpdaterStatus.Checking |
||||
|
}) |
||||
|
if (import.meta.env.PROD) { |
||||
|
api.callLong("UpdaterCommand.checkForUpdates") |
||||
|
} |
||||
|
return { |
||||
|
status: curStatus, |
||||
|
speed: speed, |
||||
|
percent: percent, |
||||
|
all: all, |
||||
|
now: now, |
||||
|
isNeedUpdate, |
||||
|
checkForUpdates() { |
||||
|
if (curStatus.value === ApiUpdaterStatus.Checking) return |
||||
|
if (curStatus.value === ApiUpdaterStatus.Downloading) return |
||||
|
curStatus.value = ApiUpdaterStatus.StartChecking |
||||
|
api.callLong("UpdaterCommand.checkForUpdates") |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
persist: false, |
||||
|
}, |
||||
|
) |
||||
@ -1,3 +0,0 @@ |
|||||
export function useTest() { |
|
||||
console.log("test") |
|
||||
} |
|
||||
@ -0,0 +1,38 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { moduleArray } from "./Template/all" |
||||
|
let store = usePageStore() |
||||
|
const cacheList = computed(() => store.cache) |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="flex h-full dev-filetree"> |
||||
|
<div class="w-200px px-5 py-5 flex flex-col gap-1 bg-white relative" style="border-right: 1px solid #e5e8ea"> |
||||
|
<router-link to="/Template"> |
||||
|
首页 |
||||
|
</router-link> |
||||
|
<router-link class="truncate" :title="item.title" :to="item.url" v-for="item in moduleArray" |
||||
|
:key="item.title"> |
||||
|
{{ item.title }} |
||||
|
</router-link> |
||||
|
<AdjustLine></AdjustLine> |
||||
|
</div> |
||||
|
<div class="flex-1 w-0 relative"> |
||||
|
<router-view v-slot="{ Component, route }"> |
||||
|
<!-- 缓存界面 --> |
||||
|
<keep-alive :include="cacheList"> |
||||
|
<component :key="route.meta.parentPath ? route.meta.parentPath : route.fullPath" :is="Component" /> |
||||
|
</keep-alive> |
||||
|
<div class="flex items-center justify-center h-full text-2xl" v-if="!Component">请选中侧边栏查看详情</div> |
||||
|
</router-view> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.dev-filetree { |
||||
|
:deep(.router-link-exact-active) { |
||||
|
color: red; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<template> |
||||
|
<IPhone> |
||||
|
asdd |
||||
|
</IPhone> |
||||
|
</template> |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"url": "/Template/Canvas", |
||||
|
"title": "Canvas" |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
const allModules = import.meta.glob("./*/params.json", { eager: true }) |
||||
|
|
||||
|
export const moduleArray: any[] = [] |
||||
|
|
||||
|
for (const key in allModules) { |
||||
|
const m = allModules[key] as any |
||||
|
const mod = m.default || m |
||||
|
moduleArray.push(mod) |
||||
|
} |
||||
|
|
||||
|
console.log(moduleArray); |
||||
|
|
||||
|
export { |
||||
|
allModules, |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
// import { useUpdaterStore } from "common/event/Updater/hook" |
||||
|
|
||||
|
// const UpdaterStore = useUpdaterStore() |
||||
|
|
||||
|
const code = ref("") |
||||
|
const aa = ref(true) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div gap="20px" h-full> |
||||
|
<input type="text" v-model="code" placeholder="请输入文本" class="input" /> |
||||
|
<input type="checkbox" v-model="aa" /> |
||||
|
{{ aa }} |
||||
|
<div h-300px> |
||||
|
<code-editor-plus :editorOptions="{ readOnly: aa }" filename="test.js" v-model="code" placeholder="请输入文本"></code-editor-plus> |
||||
|
</div> |
||||
|
<div p="20px" flex flex-wrap items-start gap="20px" justify-start rounded> |
||||
|
<Card v-for="i in 20" :key="i"></Card> |
||||
|
<button class="button">Button</button> |
||||
|
<!-- <button @click="UpdaterStore.checkForUpdates()">更新</button> --> |
||||
|
<button @click="$router.push('/browser')">浏览器</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,53 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
// import { useUpdaterStore } from "common/event/Updater/hook" |
||||
|
|
||||
|
// const UpdaterStore = useUpdaterStore() |
||||
|
|
||||
|
const code = ref("") |
||||
|
const aa = ref(true) |
||||
|
const placeholder = function(){ |
||||
|
const fragment = document.createDocumentFragment() |
||||
|
// 创建文本节点 |
||||
|
const textNode = document.createTextNode("请输入文本" + " ") |
||||
|
fragment.appendChild(textNode) |
||||
|
|
||||
|
// 创建链接 |
||||
|
const link = document.createElement("a") |
||||
|
link.href = "https://baidu.com" |
||||
|
link.textContent = "AA阿松大" |
||||
|
link.style.pointerEvents = "auto" // 只对链接启用指针事件 |
||||
|
link.style.color = "#0066cc" // 设置链接颜色 |
||||
|
link.style.textDecoration = "underline" |
||||
|
link.style.cursor = "pointer" |
||||
|
link.target = "_blank" // 在新标签页打开 |
||||
|
link.rel = "noopener noreferrer" // 安全属性 |
||||
|
|
||||
|
// 阻止点击链接时编辑器获得焦点 |
||||
|
link.addEventListener("mousedown", e => { |
||||
|
e.preventDefault() |
||||
|
e.stopPropagation() |
||||
|
}) |
||||
|
|
||||
|
link.addEventListener("click", e => { |
||||
|
e.preventDefault() |
||||
|
e.stopPropagation() |
||||
|
// window.open(link.href, "_blank", "noopener,noreferrer") |
||||
|
}) |
||||
|
|
||||
|
fragment.appendChild(link) |
||||
|
return fragment |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div gap="20px" h-full> |
||||
|
<input type="text" v-model="code" placeholder="请输入文本" class="input" /> |
||||
|
<input type="checkbox" v-model="aa" /> |
||||
|
{{ aa }}📁 |
||||
|
<div h-300px> |
||||
|
<me-code :editorOptions="{ readOnly: aa }" filename="test.ts" v-model="code" :placeholder="placeholder"></me-code> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -1,7 +1,15 @@ |
|||||
<script setup lang="ts"></script> |
<script setup lang="ts"> |
||||
|
// import { useUpdaterStore } from "common/event/Updater/hook" |
||||
|
|
||||
|
// const UpdaterStore = useUpdaterStore() |
||||
|
|
||||
|
const code = ref("") |
||||
|
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<div h-full flex flex-col>sad</div> |
<div gap="20px" h-full> |
||||
|
阿松大 |
||||
|
</div> |
||||
</template> |
</template> |
||||
|
|
||||
<style lang="scss" scoped></style> |
<style lang="scss" scoped></style> |
||||
|
|||||
@ -0,0 +1,136 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import Simplebar from "simplebar-vue" |
||||
|
|
||||
|
const settingStore = useApiSetting() |
||||
|
|
||||
|
const router = useRouter() |
||||
|
|
||||
|
const active = ref(0) |
||||
|
|
||||
|
const allApp = reactive([ |
||||
|
{ label: "基础设置", path: "/setting/" }, |
||||
|
{ label: "更新设置", path: "/setting/update" }, |
||||
|
{ label: "开发设置", path: "/setting/dev" }, |
||||
|
]) |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
const currentPath = router.currentRoute.value.path |
||||
|
const index = allApp.findIndex(app => app.path === currentPath) |
||||
|
if (index !== -1) { |
||||
|
active.value = index |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
function onClick(app: any, index: number) { |
||||
|
active.value = index |
||||
|
router.replace(app.path) |
||||
|
} |
||||
|
|
||||
|
function onClickSave() { |
||||
|
settingStore |
||||
|
.save() |
||||
|
.then(() => { |
||||
|
toast("设置已保存", { type: "success" }) |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
toast("保存失败,请重试", { type: "error" }) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function onClickReset() { |
||||
|
settingStore.reset() |
||||
|
toast("设置已重置", { type: "info" }) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div h-full flex> |
||||
|
<div w="100px" h-full relative max-w="500px" min-w="100px"> |
||||
|
<Simplebar h-full> |
||||
|
<div |
||||
|
v-for="(app, index) in allApp" |
||||
|
:key="index" |
||||
|
p="8px 10px" |
||||
|
text="12px" |
||||
|
border |
||||
|
border-b |
||||
|
h="30px" |
||||
|
cursor="pointer" |
||||
|
hover:bg-gray-50 |
||||
|
class="item" |
||||
|
transition-all |
||||
|
:class="{ active: active === index }" |
||||
|
@click="onClick(app, index)" |
||||
|
> |
||||
|
<div class="text" transition-all position="absolute" left="10px">{{ app.label }}</div> |
||||
|
</div> |
||||
|
</Simplebar> |
||||
|
<AdjustLine></AdjustLine> |
||||
|
</div> |
||||
|
<div class="content" relative b-l="1px solid #E5E5E5" flex-1 w-0 overflow-auto flex flex-col> |
||||
|
<RouterView v-slot="{ Component, route }"> |
||||
|
<Transition name="slide-fade" mode="out-in"> |
||||
|
<component :is="Component" :key="route.fullPath" /> |
||||
|
</Transition> |
||||
|
</RouterView> |
||||
|
</div> |
||||
|
<div v-if="!settingStore.isSame" absolute bottom-20px right-20px flex flex-col gap-20px> |
||||
|
<div |
||||
|
:disabled="settingStore.isSaving" |
||||
|
shadow |
||||
|
flex-center |
||||
|
cursor-pointer |
||||
|
rounded="50%" |
||||
|
p="10px" |
||||
|
bg="blue-500" |
||||
|
color="white" |
||||
|
@click="onClickSave" |
||||
|
> |
||||
|
<icon-material-symbols:save></icon-material-symbols:save> |
||||
|
</div> |
||||
|
<div :disabled="settingStore.isSaving" shadow flex-center cursor-pointer rounded="50%" p="10px" bg="white" @click="onClickReset"> |
||||
|
<icon-ix:reset></icon-ix:reset> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.item { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
white-space: nowrap; |
||||
|
&::before { |
||||
|
content: ""; |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
top: 0; |
||||
|
height: 100%; |
||||
|
width: 6px; |
||||
|
background-color: #f3f4f6; |
||||
|
transition: all linear 300ms; |
||||
|
} |
||||
|
&:hover { |
||||
|
&::before { |
||||
|
width: 30px; |
||||
|
} |
||||
|
.text { |
||||
|
left: 20px; |
||||
|
} |
||||
|
} |
||||
|
&.active { |
||||
|
@apply: text-black; |
||||
|
&::before { |
||||
|
width: 100%; |
||||
|
} |
||||
|
.text { |
||||
|
left: 50%; |
||||
|
transform: translateX(-50%); |
||||
|
} |
||||
|
} |
||||
|
.text { |
||||
|
transition-duration: 300ms; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,42 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
const settingStore = useApiSetting() |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div h-full> |
||||
|
<div class="form"> |
||||
|
<div class="form-item" :class="{ ['not-save']: settingStore.diffKeys.includes('dev:debug') }"> |
||||
|
<div class="form-item__label">存储地址</div> |
||||
|
<div class="form-item__value"> |
||||
|
<div class="select"> |
||||
|
<select v-model="settingStore.config['dev:debug']"> |
||||
|
<option :value="0">开启调试模式</option> |
||||
|
<option :value="2">关闭调试模式</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.form { |
||||
|
padding: 20px; |
||||
|
.form-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
+ .form-item { |
||||
|
margin-top: 15px; |
||||
|
} |
||||
|
.form-item__label { |
||||
|
width: 100px; |
||||
|
font-weight: bold; |
||||
|
flex-basis: 100px; |
||||
|
} |
||||
|
.form-item__value { |
||||
|
width: 600px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,18 +1,134 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
const SettingStore = useApiSetting() |
||||
|
const ApiPlatForm = useApiPlatForm() |
||||
|
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<div h-full flex> |
<div h-full> |
||||
<div w="200px" border-r="#E5E5E5 solid 1px">左侧菜单</div> |
<div class="form"> |
||||
<div flex="1" w="0"> |
<div class="form-item" :class="{ ['not-save']: SettingStore.diffKeys.includes('storagePath') }"> |
||||
{{ settingStore.isSame }}--{{ settingStore.diffKeys }} |
<div class="form-item__label">存储地址</div> |
||||
<input v-model="settingStore.config['update.repo']" type="text" /> |
<div class="form-item__value" flex gap="10px" items-center> |
||||
{{ settingStore.config["update.repo"] }} |
<div class="input-wrapper"> |
||||
<button v-if="!settingStore.isSame" :disabled="settingStore.isSaving" @click="settingStore.save()">保存</button> |
<input v-model="SettingStore.config['storagePath']" class="input" readonly type="text" placeholder="请输入存储地址" /> |
||||
|
</div> |
||||
|
<button class="button" @click="ApiPlatForm.power.openDir(SettingStore.config['storagePath'])">打开</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-item" :class="{ ['not-save']: SettingStore.diffKeys.includes('common.theme') }"> |
||||
|
<div class="form-item__label">主题</div> |
||||
|
<div class="form-item__value"> |
||||
|
<div class="radio-group"> |
||||
|
<div |
||||
|
class="radio" |
||||
|
:class="{ active: SettingStore.config['common.theme'] === 'auto' }" |
||||
|
@click="SettingStore.config['common.theme'] = 'auto'" |
||||
|
> |
||||
|
Auto |
||||
|
</div> |
||||
|
<div |
||||
|
class="radio" |
||||
|
:class="{ active: SettingStore.config['common.theme'] === 'light' }" |
||||
|
@click="SettingStore.config['common.theme'] = 'light'" |
||||
|
> |
||||
|
亮色 |
||||
|
</div> |
||||
|
<div |
||||
|
class="radio" |
||||
|
:class="{ active: SettingStore.config['common.theme'] === 'dark' }" |
||||
|
@click="SettingStore.config['common.theme'] = 'dark'" |
||||
|
> |
||||
|
暗色 |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-item" :class="{ ['not-save']: SettingStore.diffKeys.includes('language') }"> |
||||
|
<div class="form-item__label">语言</div> |
||||
|
<div class="form-item__value"> |
||||
|
<div class="radio-group"> |
||||
|
<div |
||||
|
class="radio" |
||||
|
:class="{ active: SettingStore.config['language'] === 'zh' }" |
||||
|
@click="SettingStore.config['language'] = 'zh'" |
||||
|
> |
||||
|
中文 |
||||
|
</div> |
||||
|
<div |
||||
|
class="radio" |
||||
|
:class="{ active: SettingStore.config['language'] === 'en' }" |
||||
|
@click="SettingStore.config['language'] = 'en'" |
||||
|
> |
||||
|
English |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
</template> |
</template> |
||||
|
|
||||
<script setup lang="ts"> |
<style lang="scss" scoped> |
||||
import { useSettingStore } from "common/event/Setting/hook" |
.form { |
||||
|
padding: 20px; |
||||
const settingStore = useSettingStore() |
.form-item { |
||||
console.log(settingStore.config) |
display: flex; |
||||
</script> |
align-items: center; |
||||
|
+ .form-item { |
||||
|
margin-top: 15px; |
||||
|
} |
||||
|
.form-item__label { |
||||
|
width: 100px; |
||||
|
font-weight: bold; |
||||
|
flex-basis: 100px; |
||||
|
} |
||||
|
.form-item__value { |
||||
|
width: 600px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.submit { |
||||
|
margin: 20px; |
||||
|
padding: 10px 20px; |
||||
|
background-color: #007bff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 5px; |
||||
|
cursor: pointer; |
||||
|
&:disabled { |
||||
|
background-color: #ccc; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
} |
||||
|
.input-wrapper { |
||||
|
width: 400px; |
||||
|
transition: width 0.3s ease; |
||||
|
&:focus-within { |
||||
|
width: 600px; |
||||
|
} |
||||
|
} |
||||
|
.radio-group { |
||||
|
display: inline-flex; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 5px; |
||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
||||
|
.radio { |
||||
|
flex: 1; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 10px; |
||||
|
&:hover { |
||||
|
background-color: #f0f0f0; |
||||
|
} |
||||
|
&.active { |
||||
|
background-color: #e0e0e0; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
+ .radio { |
||||
|
border-left: 1px solid #ccc; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|||||
@ -0,0 +1,72 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div h-full flex>dasd</div> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.form { |
||||
|
padding: 20px; |
||||
|
.form-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
+ .form-item { |
||||
|
margin-top: 15px; |
||||
|
} |
||||
|
.form-item__label { |
||||
|
width: 100px; |
||||
|
font-weight: bold; |
||||
|
flex-basis: 100px; |
||||
|
} |
||||
|
.form-item__value { |
||||
|
width: 300px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.submit { |
||||
|
margin: 20px; |
||||
|
padding: 10px 20px; |
||||
|
background-color: #007bff; |
||||
|
color: white; |
||||
|
border: none; |
||||
|
border-radius: 5px; |
||||
|
cursor: pointer; |
||||
|
&:disabled { |
||||
|
background-color: #ccc; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
} |
||||
|
// .input-wrapper { |
||||
|
// .input { |
||||
|
// width: 100%; |
||||
|
// padding: 8px; |
||||
|
// border: 1px solid #ccc; |
||||
|
// border-radius: 4px; |
||||
|
// } |
||||
|
// } |
||||
|
.radio-group { |
||||
|
display: inline-flex; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 5px; |
||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
||||
|
.radio { |
||||
|
flex: 1; |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 10px; |
||||
|
&:hover { |
||||
|
background-color: #f0f0f0; |
||||
|
} |
||||
|
&.active { |
||||
|
background-color: #e0e0e0; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
+ .radio { |
||||
|
border-left: 1px solid #ccc; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,24 @@ |
|||||
|
import { defineStore } from "pinia" |
||||
|
|
||||
|
export const usePageStore = defineStore("page", { |
||||
|
state: (): { _cache: string[] } => ({ |
||||
|
_cache: [], |
||||
|
}), |
||||
|
getters: { |
||||
|
cache: state => state._cache, |
||||
|
}, |
||||
|
actions: { |
||||
|
addCacheView(name: string) { |
||||
|
if (!this._cache.includes(name)) { |
||||
|
this._cache.push(name) |
||||
|
} |
||||
|
}, |
||||
|
removeCacheView(name: string) { |
||||
|
let index = this.cache.indexOf(name) |
||||
|
if (index > -1) { |
||||
|
this._cache.splice(index, 1) |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
export default usePageStore |
||||
@ -0,0 +1,76 @@ |
|||||
|
<template> |
||||
|
<div |
||||
|
v-if="UpdaterStore.isNeedUpdate" |
||||
|
text-sm |
||||
|
px-2 |
||||
|
py-1 |
||||
|
flex |
||||
|
items-center |
||||
|
hover:bg-gray-2 |
||||
|
hover:cursor-pointer |
||||
|
text="hover:hover" |
||||
|
@click="UpdaterStore.checkForUpdates" |
||||
|
> |
||||
|
<icon-grommet-icons:update |
||||
|
v-if=" |
||||
|
UpdaterStore.status === ApiUpdaterStatus.StartChecking || |
||||
|
UpdaterStore.status === ApiUpdaterStatus.Checking || |
||||
|
UpdaterStore.status === ApiUpdaterStatus.UpdateAvailable |
||||
|
" |
||||
|
:class="{ rotate: UpdaterStore.status === ApiUpdaterStatus.Checking }" |
||||
|
></icon-grommet-icons:update> |
||||
|
<icon-bxs:error v-if="UpdaterStore.status === ApiUpdaterStatus.Error" title="更新失败" class="text-red-400"></icon-bxs:error> |
||||
|
</div> |
||||
|
<div |
||||
|
v-if="UpdaterStore.status === ApiUpdaterStatus.Downloading" |
||||
|
class="progress" |
||||
|
style="font-size: 12px" |
||||
|
px-2 |
||||
|
flex |
||||
|
items-center |
||||
|
bg-gray-2 |
||||
|
hover:cursor-pointer |
||||
|
text="hover:hover" |
||||
|
> |
||||
|
<div class="line" :style="{ width: UpdaterStore.percent + '%' }"></div> |
||||
|
<div class="speed">{{ UpdaterStore.speed }} KB/s</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
const UpdaterStore = useApiUpdater() |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.rotate { |
||||
|
animation: rotate 1.5s linear infinite forwards running; |
||||
|
} |
||||
|
|
||||
|
@keyframes rotate { |
||||
|
from { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
to { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
|
.progress { |
||||
|
background-color: rgba(229, 231, 235, 100); |
||||
|
position: relative; |
||||
|
.speed { |
||||
|
position: relative; |
||||
|
z-index: 3; |
||||
|
color: red; |
||||
|
} |
||||
|
.line { |
||||
|
content: ""; |
||||
|
z-index: 2; |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
top: 0; |
||||
|
bottom: 0; |
||||
|
width: 0; |
||||
|
background-color: rgb(239, 156, 156); |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,3 @@ |
|||||
|
provider: "github" |
||||
|
owner: "npmrun" |
||||
|
repo: "wood-desktop" |
||||
Loading…
Reference in new issue