diff --git a/eslint.config.ts b/eslint.config.ts index 5ac0583d..4a8483a2 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -8,6 +8,11 @@ export default ts.config( ts.configs.strictTypeChecked, ts.configs.stylisticTypeChecked, vue.configs["flat/recommended"], + { + rules: { + "vue/no-v-html": "off", // Disable v-html warning as we need it for formatted text + }, + }, { // FIXME: remove this once we no longer need the bracket fix ignores: ["**/*.js"], @@ -19,6 +24,19 @@ export default ts.config( parser: ts.parser, projectService: true, }, + globals: { + window: "readonly", + document: "readonly", + navigator: "readonly", + setTimeout: "readonly", + console: "readonly", + alert: "readonly", + fetch: "readonly", + ClipboardItem: "readonly", + }, + // Add browser environment + ecmaVersion: 2022, + sourceType: "module", }, }, prettier, diff --git a/package.json b/package.json index b54305a2..ced059ff 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ }, "dependencies": { "@primeuix/themes": "^1.0.3", - "primevue": "^4.3.3" + "dom-to-image-more": "^3.5.0", + "primevue": "^4.3.3", + "qrcodejs2": "^0.0.2" }, "devDependencies": { "@modyfi/vite-plugin-yaml": "^1.1.1", diff --git a/src/.vitepress/theme/index.ts b/src/.vitepress/theme/index.ts index 117cc74c..75ede8a8 100644 --- a/src/.vitepress/theme/index.ts +++ b/src/.vitepress/theme/index.ts @@ -5,13 +5,20 @@ import DefaultTheme from "vitepress/theme"; import Aura from "@primeuix/themes/aura"; import PrimeVue from "primevue/config"; + +// Import components +import AppLayout from "@/components/AppLayout.vue"; +import SelectionShareMenu from "@/components/SelectionShareMenu.vue"; import PostList from "@/components/PostList.vue"; import redirect from "@/redirect"; import "./style.css"; +// Define the theme export default { extends: DefaultTheme, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + Layout: AppLayout as any, enhanceApp({ app, router }) { /* client side redirections */ router.onBeforeRouteChange = (to) => redirect(to, router); @@ -21,5 +28,6 @@ export default { /* register global components */ app.component("PostList", PostList); + app.component("SelectionShareMenu", SelectionShareMenu); }, } as Theme; diff --git a/src/components/AppLayout.vue b/src/components/AppLayout.vue new file mode 100644 index 00000000..9410ee19 --- /dev/null +++ b/src/components/AppLayout.vue @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/components/SelectionShareMenu.vue b/src/components/SelectionShareMenu.vue new file mode 100644 index 00000000..419260d5 --- /dev/null +++ b/src/components/SelectionShareMenu.vue @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/ShareCard.vue b/src/components/ShareCard.vue new file mode 100644 index 00000000..d39b8e10 --- /dev/null +++ b/src/components/ShareCard.vue @@ -0,0 +1,678 @@ + + + + + + + 分享内容 + × + + + + + + + + + + + + + + + + + 主题: + + A + + + + + {{ isGeneratingImage ? "生成中..." : "复制到剪贴板" }} + + + {{ isGeneratingImage ? "生成中..." : "下载分享图片" }} + + + + + + + + diff --git a/src/types/dom-to-image-more.d.ts b/src/types/dom-to-image-more.d.ts new file mode 100644 index 00000000..11f69b72 --- /dev/null +++ b/src/types/dom-to-image-more.d.ts @@ -0,0 +1,14 @@ +declare module 'dom-to-image-more' { + export interface DomToImageOptions { + quality?: number; + bgcolor?: string; + width?: number; + height?: number; + style?: Record; + } + + export function toPng(node: HTMLElement, options?: DomToImageOptions): Promise; + export function toJpeg(node: HTMLElement, options?: DomToImageOptions): Promise; + export function toBlob(node: HTMLElement, options?: DomToImageOptions): Promise; + export function toSvg(node: HTMLElement, options?: DomToImageOptions): Promise; +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 00000000..fdb8c0a5 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,46 @@ +// This file declares global browser objects to prevent ESLint errors +// when accessing browser globals like window, document, etc. + +interface Window { + getSelection(): Selection | null; + location: Location; + dataLayer: unknown[]; +} + +// Clipboard API declarations +interface ClipboardItemOptions { + presentationStyle?: "unspecified" | "inline" | "attachment"; +} + +interface ClipboardItem { + readonly types: string[]; + getType(type: string): Promise; +} + +interface ClipboardItemConstructor { + new ( + items: Record>, + options?: ClipboardItemOptions, + ): ClipboardItem; + supports(type: string): boolean; +} + +interface Clipboard { + read(): Promise; + readText(): Promise; + write(items: ClipboardItem[]): Promise; + writeText(text: string): Promise; +} + +type ClipboardItems = ClipboardItem[]; + +declare function setTimeout(callback: () => void, ms?: number): number; +declare function alert(message?: string): void; +declare const console: Console; + +// Make TypeScript aware that we're in a browser environment +declare const window: Window; +declare const document: Document; +declare const navigator: Navigator & { clipboard: Clipboard }; +declare const fetch: typeof globalThis.fetch; +declare const ClipboardItem: ClipboardItemConstructor;