diff --git a/electron.vite.config.ts b/electron.vite.config.ts index e7b246c..57cc81e 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -104,7 +104,7 @@ export default defineConfig({ "vue-i18n", ], dts: true, - dirs: ["src/composables/**/*.ts", "!src/composables/**/_/**/*.ts"], + dirs: ["src/composables/**/*.ts", "!src/composables/**/_/**/*.ts", "src/store/module/**/*.ts"], vueTemplate: true, }), // https://github.com/antfu/vite-plugin-components diff --git a/src/renderer/auto-imports.d.ts b/src/renderer/auto-imports.d.ts index 395fe51..3797b14 100644 --- a/src/renderer/auto-imports.d.ts +++ b/src/renderer/auto-imports.d.ts @@ -69,6 +69,7 @@ declare global { const onUnmounted: typeof import('vue')['onUnmounted'] const onUpdated: typeof import('vue')['onUpdated'] const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const page: typeof import('./src/store/module/page')['default'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const provide: typeof import('vue')['provide'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] @@ -213,6 +214,7 @@ declare global { const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] const useOnline: typeof import('@vueuse/core')['useOnline'] const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] + const usePageStore: typeof import('./src/store/module/page')['usePageStore'] const useParallax: typeof import('@vueuse/core')['useParallax'] const useParentElement: typeof import('@vueuse/core')['useParentElement'] const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] @@ -374,6 +376,7 @@ declare module 'vue' { readonly onUnmounted: UnwrapRef readonly onUpdated: UnwrapRef readonly onWatcherCleanup: UnwrapRef + readonly page: UnwrapRef readonly pausableWatch: UnwrapRef readonly provide: UnwrapRef readonly provideLocal: UnwrapRef @@ -518,6 +521,7 @@ declare module 'vue' { readonly useOffsetPagination: UnwrapRef readonly useOnline: UnwrapRef readonly usePageLeave: UnwrapRef + readonly usePageStore: UnwrapRef readonly useParallax: UnwrapRef readonly useParentElement: UnwrapRef readonly usePerformanceObserver: UnwrapRef diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 5445c71..f895442 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -17,6 +17,8 @@ declare module 'vue' { 'IconIx:reset': typeof import('~icons/ix/reset')['default'] 'IconMaterialSymbols:save': typeof import('~icons/material-symbols/save')['default'] 'IconStash:arrowReplyDuotone': typeof import('~icons/stash/arrow-reply-duotone')['default'] + IPhone: typeof import('./src/components/IPhone.vue')['default'] + MeCode: typeof import('./src/components/MeCode/index.vue')['default'] NavBar: typeof import('./src/ui/NavBar.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts b/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts index ce5e65a..cddae85 100644 --- a/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts +++ b/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts @@ -10,7 +10,7 @@ export class PlaceholderContentWidget implements monaco.editor.IContentWidget { private domNode: HTMLElement | undefined constructor( - private readonly placeholder: string, + private readonly placeholder: (() => Node) | string, private readonly editor: monaco.editor.ICodeEditor, ) { // register a listener for editor code changes @@ -39,34 +39,7 @@ export class PlaceholderContentWidget implements monaco.editor.IContentWidget { this.domNode.style.fontStyle = "italic" this.domNode.style.opacity = "0.6" // 添加透明度,更像 placeholder - // 创建文本节点 - const textNode = document.createTextNode(this.placeholder + " ") - this.domNode.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") - }) - - this.domNode.appendChild(link) + this.domNode.appendChild(typeof this.placeholder === "function" ? this.placeholder() : document.createTextNode(this.placeholder)) this.editor.applyFontInfo(this.domNode) } diff --git a/src/renderer/src/components/CodeEditor/code-editor-plus.vue b/src/renderer/src/components/CodeEditor/code-editor-plus.vue index 7de95c4..a0611e2 100644 --- a/src/renderer/src/components/CodeEditor/code-editor-plus.vue +++ b/src/renderer/src/components/CodeEditor/code-editor-plus.vue @@ -6,7 +6,7 @@ readonly?: boolean modelValue?: string filename?: string - placeholder?: string + placeholder?: (() => Node) | string modelOptions?: IOptions["modelOptions"] editorOptions?: IOptions["editorOptions"] }>(), @@ -25,7 +25,7 @@ const editorRef = ref() const { updateOption, setValue } = useMonacoEditor(editorRef, { - placeholder: "请输入一些文本测试", + placeholder: props.placeholder, content: props.modelValue, filename: props.filename, modelOptions: props.modelOptions, diff --git a/src/renderer/src/components/CodeEditor/hook.ts b/src/renderer/src/components/CodeEditor/hook.ts index 5c81104..e25c4f6 100644 --- a/src/renderer/src/components/CodeEditor/hook.ts +++ b/src/renderer/src/components/CodeEditor/hook.ts @@ -40,7 +40,7 @@ function useResizeObserver(el: HTMLDivElement, callback: ResizeObserverCallback) } export interface IOptions { - placeholder: string + placeholder: (() => Node) | string | undefined filename: string content: string editorOptions: monaco.editor.IEditorOptions & monaco.editor.IGlobalEditorOptions @@ -50,7 +50,7 @@ export interface IOptions { } const defaultOptions: IOptions = { - placeholder: "请输入文本", + placeholder: undefined, filename: "temp", content: "", editorOptions: { @@ -175,7 +175,7 @@ export function useMonacoEditor(editorElement: Ref, curOption.onCursorChange?.(e) }) if (!curOption.content) { - placeholderWidget = new PlaceholderContentWidget(curOption.placeholder, editor) + placeholderWidget = new PlaceholderContentWidget(curOption.placeholder || "", editor) } else { if (isInnerChange === "waitting") { isInnerChange = "out" diff --git a/src/renderer/src/components/IPhone.vue b/src/renderer/src/components/IPhone.vue new file mode 100644 index 0000000..db8b878 --- /dev/null +++ b/src/renderer/src/components/IPhone.vue @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/src/components/MeCode/PlaceholderContentWidget.ts b/src/renderer/src/components/MeCode/PlaceholderContentWidget.ts new file mode 100644 index 0000000..cddae85 --- /dev/null +++ b/src/renderer/src/components/MeCode/PlaceholderContentWidget.ts @@ -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) + } +} diff --git a/src/renderer/src/components/MeCode/hook.ts b/src/renderer/src/components/MeCode/hook.ts new file mode 100644 index 0000000..e2b2436 --- /dev/null +++ b/src/renderer/src/components/MeCode/hook.ts @@ -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, opts: Partial) { + let curOption = getOptions(opts) as IOptions + let editor: monaco.editor.IStandaloneCodeEditor | null = null + let placeholderWidget: PlaceholderContentWidget | null = null + + const updateOption = (opts: Partial) => { + 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, + } +} diff --git a/src/renderer/src/components/MeCode/index.vue b/src/renderer/src/components/MeCode/index.vue new file mode 100644 index 0000000..ad4c322 --- /dev/null +++ b/src/renderer/src/components/MeCode/index.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/renderer/src/components/MeCode/monaco.ts b/src/renderer/src/components/MeCode/monaco.ts new file mode 100644 index 0000000..5fae0e8 --- /dev/null +++ b/src/renderer/src/components/MeCode/monaco.ts @@ -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 } diff --git a/src/renderer/src/components/MeCode/utils.ts b/src/renderer/src/components/MeCode/utils.ts new file mode 100644 index 0000000..94a2386 --- /dev/null +++ b/src/renderer/src/components/MeCode/utils.ts @@ -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 +} diff --git a/src/renderer/src/pages/Template.vue b/src/renderer/src/pages/Template.vue new file mode 100644 index 0000000..bff7a44 --- /dev/null +++ b/src/renderer/src/pages/Template.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/src/pages/Template/Canvas/index.vue b/src/renderer/src/pages/Template/Canvas/index.vue new file mode 100644 index 0000000..5e5169c --- /dev/null +++ b/src/renderer/src/pages/Template/Canvas/index.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/renderer/src/pages/Template/Canvas/params.json b/src/renderer/src/pages/Template/Canvas/params.json new file mode 100644 index 0000000..b56ab6c --- /dev/null +++ b/src/renderer/src/pages/Template/Canvas/params.json @@ -0,0 +1,4 @@ +{ + "url": "/Template/Canvas", + "title": "Canvas" +} \ No newline at end of file diff --git a/src/renderer/src/pages/Template/all.ts b/src/renderer/src/pages/Template/all.ts new file mode 100644 index 0000000..f624ab6 --- /dev/null +++ b/src/renderer/src/pages/Template/all.ts @@ -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, +} \ No newline at end of file diff --git a/src/renderer/src/pages/index/index copy 2.vue b/src/renderer/src/pages/index/index copy 2.vue new file mode 100644 index 0000000..3d8846a --- /dev/null +++ b/src/renderer/src/pages/index/index copy 2.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/renderer/src/pages/index/index copy 3.vue b/src/renderer/src/pages/index/index copy 3.vue new file mode 100644 index 0000000..0d90acb --- /dev/null +++ b/src/renderer/src/pages/index/index copy 3.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/renderer/src/pages/index/index.vue b/src/renderer/src/pages/index/index.vue index 3d8846a..eabc982 100644 --- a/src/renderer/src/pages/index/index.vue +++ b/src/renderer/src/pages/index/index.vue @@ -4,23 +4,11 @@ // const UpdaterStore = useUpdaterStore() const code = ref("") - const aa = ref(true) diff --git a/src/renderer/src/store/module/page.ts b/src/renderer/src/store/module/page.ts new file mode 100644 index 0000000..9a7bbb1 --- /dev/null +++ b/src/renderer/src/store/module/page.ts @@ -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 \ No newline at end of file diff --git a/src/renderer/src/ui/NavBar.vue b/src/renderer/src/ui/NavBar.vue index 2d411cc..0e94f8c 100644 --- a/src/renderer/src/ui/NavBar.vue +++ b/src/renderer/src/ui/NavBar.vue @@ -82,6 +82,12 @@ }, }, { + label: "模板", + async click() { + router.push("/Template") + }, + }, + { label: t("browser.navbar.menu.toggleDevTools"), async click() { PlatForm.power.toggleDevTools() diff --git a/src/renderer/typed-router.d.ts b/src/renderer/typed-router.d.ts index 0625e78..400a53f 100644 --- a/src/renderer/typed-router.d.ts +++ b/src/renderer/typed-router.d.ts @@ -20,6 +20,8 @@ declare module 'vue-router/auto-routes' { export interface RouteNamedMap { '//': RouteRecordInfo<'//', '/', Record, Record>, '//index copy': RouteRecordInfo<'//index copy', '/index copy', Record, Record>, + '//index copy 2': RouteRecordInfo<'//index copy 2', '/index copy 2', Record, Record>, + '//index copy 3': RouteRecordInfo<'//index copy 3', '/index copy 3', Record, Record>, '/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue }, { all: ParamValue }>, 'about': RouteRecordInfo<'about', '/about', Record, Record>, '/browser': RouteRecordInfo<'/browser', '/browser', Record, Record>, @@ -28,5 +30,7 @@ declare module 'vue-router/auto-routes' { '/setting/': RouteRecordInfo<'/setting/', '/setting', Record, Record>, '/setting/dev': RouteRecordInfo<'/setting/dev', '/setting/dev', Record, Record>, '/setting/update': RouteRecordInfo<'/setting/update', '/setting/update', Record, Record>, + '/Template': RouteRecordInfo<'/Template', '/Template', Record, Record>, + '/Template/Canvas/': RouteRecordInfo<'/Template/Canvas/', '/Template/Canvas', Record, Record>, } }