├── src ├── locale │ ├── zh-CN │ │ ├── sidebar.json │ │ ├── changelog.json │ │ ├── cropper.json │ │ ├── action │ │ │ ├── setting.json │ │ │ ├── copywriting.json │ │ │ ├── user.json │ │ │ ├── template.json │ │ │ └── card.json │ │ ├── index.ts │ │ └── app.json │ └── en-US │ │ ├── sidebar.json │ │ ├── changelog.json │ │ ├── cropper.json │ │ ├── action │ │ ├── setting.json │ │ ├── copywriting.json │ │ ├── user.json │ │ ├── template.json │ │ └── card.json │ │ ├── index.ts │ │ └── app.json ├── composables │ ├── isDark.ts │ ├── useFont.ts │ ├── useBlobUrl.ts │ ├── usePost.ts │ └── usePalette.ts ├── assets │ ├── img │ │ ├── avatar.jpg │ │ ├── background.jpeg │ │ └── reference.jpg │ ├── font │ │ └── Kenney-Mini-Square.ttf │ └── vue.svg ├── templates │ ├── luyejiu │ │ ├── background.png │ │ └── manifest.jsonc │ ├── default │ │ └── manifest.jsonc │ └── index.ts ├── types │ ├── user.ts │ └── template.ts ├── utils │ ├── is.ts │ ├── dom.ts │ ├── cache.ts │ ├── canvas.ts │ ├── i18n.ts │ ├── template.ts │ ├── rgbaster.ts │ └── draw.ts ├── vite-env.d.ts ├── App.vue ├── layout │ ├── Sidebar │ │ ├── useDisabled.ts │ │ ├── Action.vue │ │ ├── SidebarPanel.vue │ │ ├── index.vue │ │ ├── ActionPanel.vue │ │ ├── Copywriting.vue │ │ ├── User.vue │ │ ├── Template.vue │ │ ├── Setting.vue │ │ └── Card.vue │ ├── index.vue │ └── Header.vue ├── store │ ├── user.ts │ ├── sidebar.ts │ ├── app.ts │ └── template.ts ├── workers │ ├── palette │ │ ├── worker.ts │ │ └── index.ts │ └── draw │ │ ├── index.ts │ │ └── worker.ts ├── components │ ├── common │ │ ├── DemoFansCard.vue │ │ ├── Palette.vue │ │ ├── WelcomeFansCard.vue │ │ ├── AvatarCropper.vue │ │ ├── ChangelogDialog.vue │ │ ├── PreviewPost.vue │ │ ├── NameDialog.vue │ │ ├── FansCard.vue │ │ └── AvatarCropperDialog.vue │ ├── ui │ │ ├── ActionFormItem.vue │ │ ├── ResponsiveButton.vue │ │ ├── Button.vue │ │ └── CropperToolbox.vue │ └── global │ │ ├── ConfigProvider.vue │ │ └── Cropper.vue ├── styles │ ├── index.css │ └── global.css ├── router │ └── index.ts ├── main.ts └── pages │ └── index.vue ├── public ├── app.icns ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── safari-pinned-tab.svg ├── vercel.json ├── tsconfig.json ├── eslint.config.js ├── .vscode └── settings.json ├── .gitignore ├── tsconfig.node.json ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── README.md ├── CHANGELOG.md ├── index.html ├── tsconfig.app.json ├── CHANGELOG_v0.md ├── uno.config.ts ├── package.json ├── components.d.ts ├── vite.config.ts └── auto-imports.d.ts /src/locale/zh-CN/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/locale/en-US/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /public/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/app.icns -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/composables/isDark.ts: -------------------------------------------------------------------------------- 1 | export const isDark = useDark({ 2 | disableTransition: false, 3 | }); 4 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/src/assets/img/avatar.jpg -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/locale/zh-CN/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "更新日志", 3 | "legacy": { 4 | "v0": "v0.x" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/img/background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/src/assets/img/background.jpeg -------------------------------------------------------------------------------- /src/assets/img/reference.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/src/assets/img/reference.jpg -------------------------------------------------------------------------------- /src/locale/en-US/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Changelog", 3 | "legacy": { 4 | "v0": "v0.x" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/templates/luyejiu/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/src/templates/luyejiu/background.png -------------------------------------------------------------------------------- /src/assets/font/Kenney-Mini-Square.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bernankez/BilibiliFans/HEAD/src/assets/font/Kenney-Mini-Square.ttf -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | nickname: string; 3 | avatar: string; 4 | no: number; 5 | date: string; 6 | } 7 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "src": "/[^.]+", 5 | "dest": "/", 6 | "status": 200 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isWebWorker() { 2 | return typeof WorkerGlobalScope !== "undefined" && globalThis instanceof WorkerGlobalScope; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.app.json" 5 | }, 6 | { 7 | "path": "./tsconfig.node.json" 8 | } 9 | ], 10 | "files": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/locale/zh-CN/cropper.json: -------------------------------------------------------------------------------- 1 | { 2 | "restrictImage": "限制图片在框选区域内", 3 | "toolbox": { 4 | "up": "向上移动图片", 5 | "down": "向下移动图片", 6 | "left": "向左移动图片", 7 | "right": "向右移动图片" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.md" { 4 | import type { ComponentOptions } from "vue"; 5 | 6 | const Component: ComponentOptions; 7 | export default Component; 8 | } 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import bernankez from "@bernankez/eslint-config"; 2 | 3 | export default bernankez({ 4 | formatters: true, 5 | unocss: true, 6 | linterOptions: { 7 | reportUnusedDisableDirectives: "off", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/layout/Sidebar/useDisabled.ts: -------------------------------------------------------------------------------- 1 | export function useDisabled() { 2 | const templateStore = useTemplateStore(); 3 | 4 | const disabled = computed(() => !templateStore.currentTemplate); 5 | 6 | return { 7 | disabled, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/locale/en-US/cropper.json: -------------------------------------------------------------------------------- 1 | { 2 | "restrictImage": "Restrict the image within the stencil", 3 | "toolbox": { 4 | "up": "Move the image up", 5 | "down": "Move the image down", 6 | "left": "Move the image left", 7 | "right": "Move the image right" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "src/locale" 4 | ], 5 | "i18n-ally.keystyle": "nested", 6 | "i18n-ally.enabledParsers": ["ts", "json", "json5"], 7 | "i18n-ally.parsers.typescript.compilerOptions": { 8 | "moduleResolution": "node" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | export const useUserStore = defineStore("user", () => { 2 | const nickname = ref(""); 3 | const avatar = ref(""); 4 | const no = ref(1); 5 | 6 | return { 7 | nickname, 8 | avatar, 9 | no, 10 | }; 11 | }, { 12 | persist: { 13 | pick: ["nickname", "avatar"], 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dev-dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/locale/zh-CN/action/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "设置", 3 | "form": { 4 | "quality": { 5 | "title": "生成图片大小", 6 | "large": "大", 7 | "medium": "中", 8 | "small": "小" 9 | }, 10 | "language": { 11 | "title": "语言" 12 | }, 13 | "theme": { 14 | "title": "主题" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/store/sidebar.ts: -------------------------------------------------------------------------------- 1 | export type Actions = "template" | "copywriting" | "card" | "user" | "setting"; 2 | 3 | export const useSidebarStore = defineStore("sidebar", () => { 4 | const show = ref(false); 5 | const activeAction = ref(); 6 | 7 | return { 8 | show, 9 | activeAction, 10 | }; 11 | }, { 12 | persist: true, 13 | }); 14 | -------------------------------------------------------------------------------- /src/locale/en-US/action/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Settings", 3 | "form": { 4 | "quality": { 5 | "title": "Image size", 6 | "large": "Large", 7 | "medium": "Medium", 8 | "small": "Small" 9 | }, 10 | "language": { 11 | "title": "Language" 12 | }, 13 | "theme": { 14 | "title": "Theme" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["vite.config.ts", "uno.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/workers/palette/worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { analyze } from "@/utils/rgbaster"; 4 | 5 | globalThis.addEventListener("message", async (e) => { 6 | const { data } = e; 7 | const { url, width, height } = data as { url: string; width: number; height: number }; 8 | const result = await analyze(url, { width, height, scale: 0.6 }); 9 | globalThis.postMessage(result); 10 | }); 11 | -------------------------------------------------------------------------------- /src/locale/zh-CN/action/copywriting.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "文案", 3 | "form": { 4 | "vtuberName": { 5 | "title": "主播名" 6 | }, 7 | "themeLink": { 8 | "title": "装扮链接", 9 | "reset": "重置" 10 | }, 11 | "post": { 12 | "title": "动态", 13 | "reset": "重置", 14 | "copy": { 15 | "success": "复制成功" 16 | } 17 | }, 18 | "preview": { 19 | "title": "预览" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export function checkVisibility(element: HTMLElement) { 2 | if (typeof element.checkVisibility === "function") { 3 | return element.checkVisibility(); 4 | } 5 | let e: HTMLElement | null = element; 6 | while (e) { 7 | // NOTE more conditions can be added 8 | if (window.getComputedStyle(e).display === "none") { 9 | return false; 10 | } 11 | e = e.parentElement; 12 | } 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /src/locale/zh-CN/action/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "你的信息", 3 | "nickname": { 4 | "title": "用户名" 5 | }, 6 | "avatar": { 7 | "title": "头像", 8 | "placeholder": "点击或拖动文件到此处上传", 9 | "typeValidate": "请上传图片文件", 10 | "cropDialog": { 11 | "title": "裁剪头像", 12 | "confirm": "确定", 13 | "cancel": "取消" 14 | } 15 | }, 16 | "no": { 17 | "title": "装扮编号" 18 | }, 19 | "date": { 20 | "title": "获得日期" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | -------------------------------------------------------------------------------- /src/locale/en-US/action/copywriting.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Copywriting", 3 | "form": { 4 | "vtuberName": { 5 | "title": "Vtuber Name" 6 | }, 7 | "themeLink": { 8 | "title": "Theme Link", 9 | "reset": "Reset" 10 | }, 11 | "post": { 12 | "title": "Post", 13 | "reset": "Reset", 14 | "copy": { 15 | "success": "Copied successfully" 16 | } 17 | }, 18 | "preview": { 19 | "title": "Preview" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/DemoFansCard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/composables/useFont.ts: -------------------------------------------------------------------------------- 1 | export function useFont(family: string, source: string | BinaryData, descriptors?: FontFaceDescriptors) { 2 | const loaded = ref(false); 3 | const font = ref(); 4 | 5 | async function load() { 6 | const _font = new FontFace(family, source, descriptors); 7 | await _font.load(); 8 | document.fonts.add(_font); 9 | loaded.value = true; 10 | font.value = _font; 11 | } 12 | 13 | load(); 14 | 15 | return { 16 | loaded, 17 | font, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/workers/palette/index.ts: -------------------------------------------------------------------------------- 1 | import type { Palette } from "@/utils/rgbaster"; 2 | import PaletteWorkerUrl from "./worker?worker&url"; 3 | 4 | export function palette(url: string, width: number, height: number) { 5 | const worker = new Worker(PaletteWorkerUrl, { 6 | type: "module", 7 | }); 8 | return new Promise((resolve) => { 9 | worker.onmessage = (e) => { 10 | resolve(e.data); 11 | worker.terminate(); 12 | }; 13 | worker.postMessage({ url, width, height }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | 22 | - run: npx changelogithub 23 | env: 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /src/locale/zh-CN/action/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "模版管理", 3 | "import": { 4 | "title": "导入模版", 5 | "error": { 6 | "invalidFile": "无效的模版文件" 7 | } 8 | }, 9 | "rename": { 10 | "title": "重命名" 11 | }, 12 | "export": { 13 | "title": "导出" 14 | }, 15 | "delete": { 16 | "title": "删除", 17 | "confirm": "确定要删除当前模版吗?", 18 | "confirmText": "确定" 19 | }, 20 | "select": { 21 | "placeholder": { 22 | "default": "默认模版", 23 | "custom": "自定义模版" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/workers/draw/index.ts: -------------------------------------------------------------------------------- 1 | import { klona } from "klona"; 2 | import type { RawDrawOptions } from "@/utils/draw"; 3 | import DrawWorkerUrl from "./worker.ts?worker&url"; 4 | 5 | export function draw(options: RawDrawOptions) { 6 | const worker = new Worker(DrawWorkerUrl, { 7 | type: "module", 8 | }); 9 | return new Promise((resolve) => { 10 | worker.onmessage = (e) => { 11 | resolve(e.data); 12 | worker.terminate(); 13 | }; 14 | worker.postMessage(klona(options)); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/locale/en-US/action/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Your Information", 3 | "nickname": { 4 | "title": "Nickname" 5 | }, 6 | "avatar": { 7 | "title": "Avatar", 8 | "placeholder": "Click or drag file here to upload", 9 | "typeValidate": "Please upload image file", 10 | "cropDialog": { 11 | "title": "Crop avatar", 12 | "confirm": "Confirm", 13 | "cancel": "Cancel" 14 | } 15 | }, 16 | "no": { 17 | "title": "NO." 18 | }, 19 | "date": { 20 | "title": "Date" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/workers/draw/worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { type RawDrawOptions, render } from "@/utils/draw"; 4 | 5 | globalThis.addEventListener("message", async (e) => { 6 | const { data: rawDrawOptions } = e; 7 | const offscreenCanvas = await render(rawDrawOptions as RawDrawOptions) as OffscreenCanvas; 8 | try { 9 | const imageBitmap = offscreenCanvas.transferToImageBitmap(); 10 | globalThis.postMessage(imageBitmap, [imageBitmap]); 11 | } catch (e) { 12 | console.warn(e); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/common/Palette.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /src/locale/en-US/action/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Template Management", 3 | "import": { 4 | "title": "Import template", 5 | "error": { 6 | "invalidFile": "Invalid template file" 7 | } 8 | }, 9 | "rename": { 10 | "title": "Rename" 11 | }, 12 | "export": { 13 | "title": "Export" 14 | }, 15 | "delete": { 16 | "title": "Delete", 17 | "confirm": "Are you sure to delete current template?", 18 | "confirmText": "Confirm" 19 | }, 20 | "select": { 21 | "placeholder": { 22 | "default": "Default templates", 23 | "custom": "Custom templates" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/locale/en-US/index.ts: -------------------------------------------------------------------------------- 1 | import card from "./action/card.json"; 2 | import copywriting from "./action/copywriting.json"; 3 | import setting from "./action/setting.json"; 4 | import template from "./action/template.json"; 5 | import user from "./action/user.json"; 6 | import app from "./app.json"; 7 | import changelog from "./changelog.json"; 8 | import cropper from "./cropper.json"; 9 | import sidebar from "./sidebar.json"; 10 | 11 | export const enUS = { 12 | app, 13 | sidebar, 14 | cropper, 15 | changelog, 16 | action: { 17 | template, 18 | copywriting, 19 | card, 20 | user, 21 | setting, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/locale/zh-CN/index.ts: -------------------------------------------------------------------------------- 1 | import card from "./action/card.json"; 2 | import copywriting from "./action/copywriting.json"; 3 | import setting from "./action/setting.json"; 4 | import template from "./action/template.json"; 5 | import user from "./action/user.json"; 6 | import app from "./app.json"; 7 | import changelog from "./changelog.json"; 8 | import cropper from "./cropper.json"; 9 | import sidebar from "./sidebar.json"; 10 | 11 | export const zhCN = { 12 | app, 13 | sidebar, 14 | cropper, 15 | changelog, 16 | action: { 17 | template, 18 | copywriting, 19 | card, 20 | user, 21 | setting, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Bilibili Fans

3 |
4 | 5 |

6 | 制作你的粉丝装扮卡片🪄 7 |

8 | 9 |

10 | 点击 bilibili-fans.keke.cc 开始制作🎉 11 |

12 | 13 |

14 | 教程 15 |

16 | 17 |

18 | 19 | 20 |

21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.1.1 2 | 3 | #### Bug Fixes 4 | 5 | - 修复在iOS设备上无法打开的问题 6 | 7 | ### v1.1.0 8 | 9 | #### Features 10 | 11 | - 现在选择背景图片后图片会压缩 12 | 13 | #### Bug Fixes 14 | 15 | - 导出的文件中不再包含模版id 16 | - 修复当没有勾选 “限制图片在框选区域内”时,导入文件后框选区域不正确问题 17 | - 修复选择背景图片后,提取图片主色导致页面暂时无响应问题 18 | 19 | ### v1.0.1 20 | 21 | #### Bug Fixes 22 | 23 | - 修复网站路径问题 24 | - 补充部分缺失的翻译 25 | 26 | ### v1.0.0 27 | 28 | #### Features 29 | 30 | - 重新设计的界面 31 | - 支持移动端 32 | - 支持多语言 33 | - 添加模版功能 34 | - 添加文件导出功能 35 | - 添加临时保存功能 36 | - 新的网站图标 37 | - 新的图片裁剪框 38 | - 现在可以自由选择生成图片大小 39 | - 精简了部分没用的设置项 40 | - 优化图片生成性能 41 | 42 | #### Bug Fixes 43 | 44 | - 修复了不同浏览器中生成图片效果略微不一致问题,现在在所有主流浏览器(Chrome、Edge、Firefox、Safari)的最新两个版本中均可用 -------------------------------------------------------------------------------- /src/locale/zh-CN/action/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "卡片样式", 3 | "form": { 4 | "fontColor": { 5 | "title": "字体颜色" 6 | }, 7 | "background": { 8 | "title": "背景色" 9 | }, 10 | "backgroundImage": { 11 | "title": "背景图片", 12 | "placeholder": "点击或拖动文件到此处上传", 13 | "typeValidate": "请上传图片文件" 14 | }, 15 | "foreground": { 16 | "title": "前景色", 17 | "left": { 18 | "title": "左侧渐变色" 19 | }, 20 | "leftGradient": { 21 | "title": "左侧渐变范围" 22 | }, 23 | "right": { 24 | "title": "右侧渐变色" 25 | }, 26 | "rightGradient": { 27 | "title": "右侧渐变范围" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/templates/default/manifest.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "id": "default", 3 | "name": "空白模版", 4 | "copywriting": { 5 | "name": "空白模版", 6 | "link": "https://www.bilibili.com/h5/mall/home?navhide=1", 7 | "post": "我是#${name}#的NO.${no}号真爱粉,靓号在手,走路带风,解锁专属粉丝卡片,使用专属粉丝装扮,你也来生成你的专属秀起来吧!${link}" 8 | }, 9 | "cardStyle": { 10 | "color": "#ffffffff", 11 | "foreground": { 12 | "gradient": { 13 | "left": { 14 | "color": "#333333ff", 15 | "start": 0.19, 16 | "end": 0.35 17 | } 18 | } 19 | }, 20 | "background": { 21 | "imageRestriction": "fit", 22 | "origin": [0, 0], 23 | "size": [1125, 463] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/ActionFormItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/store/app.ts: -------------------------------------------------------------------------------- 1 | import type { AvailableLocales } from "@/utils/i18n"; 2 | 3 | export const useAppStore = defineStore("app", () => { 4 | const locale = ref(); 5 | const split = ref(false); 6 | const splitRatio = ref(0.5); 7 | 8 | const imageSizeRef = ref(50); 9 | const imageWidth = computed(() => Math.round(1093 * imageSizeRef.value / 50)); 10 | const imageHeight = computed(() => Math.round(imageWidth.value * 0.4115)); 11 | const imageSize = computed(() => [imageWidth.value, imageHeight.value]); 12 | 13 | return { 14 | locale, 15 | split, 16 | splitRatio, 17 | 18 | imageSize, 19 | imageSizeRef, 20 | }; 21 | }, { 22 | persist: true, 23 | }); 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | BilibiliFans 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/templates/luyejiu/manifest.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "id": "luyejiu", 3 | "name": "鹿野灸", 4 | "copywriting": { 5 | "name": "鹿野灸", 6 | "link": "https://www.bilibili.com/h5/mall/suit/detail?navhide=1&id=106232701&native.theme=1&night=0", 7 | "post": "我是#${name}#的NO.${no}号真爱粉,靓号在手,走路带风,解锁专属粉丝卡片,使用专属粉丝装扮,你也来生成你的专属秀起来吧!${link}", 8 | "date": "2023/03/12" 9 | }, 10 | "cardStyle": { 11 | "color": "#ffffffff", 12 | "foreground": { 13 | "gradient": { 14 | "left": { 15 | "color": "#eaba80ff", 16 | "start": 0.19, 17 | "end": 0.35 18 | } 19 | } 20 | }, 21 | "background": { 22 | "imageRestriction": "fit", 23 | "origin": [0, 239], 24 | "size": [1040, 428] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Lint 33 | run: nr lint 34 | 35 | - name: Build 36 | run: nr build 37 | 38 | - name: Typecheck 39 | run: nr typecheck 40 | -------------------------------------------------------------------------------- /src/components/common/WelcomeFansCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/composables/useBlobUrl.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from "vue"; 2 | 3 | export function useBlobUrl(blob: MaybeRefOrGetter) { 4 | const url = ref(); 5 | watchEffect(() => { 6 | const value = toValue(blob); 7 | if (value instanceof Blob) { 8 | url.value = URL.createObjectURL(value); 9 | } else { 10 | url.value = value; 11 | } 12 | }); 13 | 14 | watch(url, (_, oldUrl) => { 15 | if (oldUrl) { 16 | revoke(oldUrl); 17 | } 18 | }, { immediate: true }); 19 | 20 | function revoke(_url?: string) { 21 | const tempUrl = _url || url.value; 22 | if (tempUrl) { 23 | URL.revokeObjectURL(tempUrl); 24 | } 25 | } 26 | 27 | return { 28 | url, 29 | revoke, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/types/template.ts: -------------------------------------------------------------------------------- 1 | export interface TemplateManifest { 2 | id: string; 3 | type: "default" | "custom"; 4 | name: string; 5 | copywriting: { 6 | name: string; 7 | link: string; 8 | post?: string; 9 | date?: string; 10 | }; 11 | cardStyle: { 12 | color: string; 13 | foreground?: { 14 | gradient: { 15 | [K in "left" | "right"]?: { 16 | start: number; 17 | end: number; 18 | color: string; 19 | }; 20 | }; 21 | }; 22 | background: { 23 | image?: T; 24 | /** Defaults to 'fit' */ 25 | imageRestriction?: "fit" | "none"; 26 | origin: [number, number]; 27 | size: [number, number]; 28 | color?: string; 29 | }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Emoji; 3 | src: local("Apple Color Emoji"), local("Segoe UI Emoji"), local("Segoe UI Symbol"), local("Noto Color Emoji"); 4 | unicode-range: U+1F000-1F644, U+203C-3299; 5 | } 6 | 7 | :root { 8 | width: 100%; 9 | height: 100%; 10 | font-family: 11 | Avenir, 12 | system-ui, 13 | -apple-system, 14 | "Segoe UI", 15 | Roboto, 16 | Emoji, 17 | Helvetica, 18 | Arial, 19 | sans-serif; 20 | 21 | font-synthesis: none; 22 | text-rendering: optimizeLegibility; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | html.dark { 28 | color-scheme: dark; 29 | } 30 | 31 | html body, 32 | #app { 33 | margin: 0; 34 | width: 100%; 35 | height: 100%; 36 | } 37 | -------------------------------------------------------------------------------- /src/locale/en-US/action/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Card Style", 3 | "form": { 4 | "fontColor": { 5 | "title": "Font Color" 6 | }, 7 | "background": { 8 | "title": "Background Color" 9 | }, 10 | "backgroundImage": { 11 | "title": "Background Image", 12 | "placeholder": "Click or drag file here to upload", 13 | "typeValidate": "Please upload image file" 14 | }, 15 | "foreground": { 16 | "title": "Foreground Color", 17 | "left": { 18 | "title": "Left Gradient Color" 19 | }, 20 | "leftGradient": { 21 | "title": "Left Gradient Range" 22 | }, 23 | "right": { 24 | "title": "Right Gradient Color" 25 | }, 26 | "rightGradient": { 27 | "title": "Right Gradient Range" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/composables/usePost.ts: -------------------------------------------------------------------------------- 1 | export function usePost() { 2 | const templateStore = useTemplateStore(); 3 | const { currentTemplate } = storeToRefs(templateStore); 4 | const userStore = useUserStore(); 5 | const { no } = storeToRefs(userStore); 6 | 7 | const matches = computed(() => [ 8 | ["name", currentTemplate.value?.copywriting.name], 9 | ["no", no.value.toString().padStart(6, "0")], 10 | ["link", currentTemplate.value?.copywriting.link], 11 | ]); 12 | 13 | const post = computed(() => { 14 | if (currentTemplate.value) { 15 | let post = currentTemplate.value.copywriting.post || ""; 16 | matches.value.forEach(([key, value]) => { 17 | post = post.replace(`$\{${key}\}`, value?.toString() ?? ""); 18 | }); 19 | return post; 20 | } 21 | return ""; 22 | }); 23 | 24 | return { 25 | post, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { type AvailableLocales, i18n, inferPreferredLocale, setLocale } from "@/utils/i18n"; 2 | import { createRouter, createWebHistory, type RouteRecordRaw, RouterView } from "vue-router"; 3 | 4 | export const routes: RouteRecordRaw[] = [ 5 | { 6 | path: "/:locale?", 7 | component: RouterView, 8 | beforeEnter: async (to, from, next) => { 9 | const lang = to.params.locale as AvailableLocales; 10 | if (!i18n.global.availableLocales.includes(lang)) { 11 | return next(inferPreferredLocale()); 12 | } 13 | 14 | await setLocale(lang); 15 | 16 | return next(); 17 | }, 18 | children: [ 19 | { 20 | path: "", 21 | component: () => import("../pages/index.vue"), 22 | }, 23 | ], 24 | }, 25 | ]; 26 | 27 | export const router = createRouter({ 28 | history: createWebHistory(), 29 | routes, 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/global/ConfigProvider.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 38 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "jsx": "preserve", 7 | "jsxImportSource": "vue", 8 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 9 | "moduleDetection": "force", 10 | "useDefineForClassFields": true, 11 | "baseUrl": ".", 12 | "module": "ESNext", 13 | 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "paths": { 17 | "@/*": ["src/*"], 18 | "~/*": ["./*"] 19 | }, 20 | "resolveJsonModule": true, 21 | "allowImportingTsExtensions": true, 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noUnusedLocals": false, 27 | "noUnusedParameters": false, 28 | "noEmit": true, 29 | "isolatedModules": true, 30 | "skipLibCheck": true 31 | }, 32 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "components.d.ts", "auto-imports.d.ts"] 33 | } 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./styles/index.css"; 3 | import "./styles/global.css"; 4 | /** The import order of vue-i18n pinia router App.vue should be fixed here */ 5 | /** Otherwise it will affect the auto import in Vue template */ 6 | /** It's because vue-i18n@9 exports ComponentCustomProperties from '@vue/runtime-core' */ 7 | /** vue-i18n@10 has re-exported ComponentCustomProperties from 'vue' */ 8 | /** After the release of vue-i18n@10, the import order here can be changed */ 9 | // eslint-disable-next-line import/order 10 | import { i18n } from "@/utils/i18n"; 11 | import { createPinia } from "pinia"; 12 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; 13 | import App from "./App.vue"; 14 | import { router } from "./router"; 15 | import "virtual:uno.css"; 16 | import "@unocss/reset/tailwind-compat.css"; 17 | 18 | const app = createApp(App); 19 | const pinia = createPinia(); 20 | pinia.use(piniaPluginPersistedstate); 21 | app.use(i18n); 22 | app.use(router); 23 | app.use(pinia); 24 | app.mount("#app"); 25 | -------------------------------------------------------------------------------- /src/components/common/AvatarCropper.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 38 | -------------------------------------------------------------------------------- /src/locale/zh-CN/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "light": "亮色", 4 | "dark": "暗色" 5 | }, 6 | "interface": { 7 | "dragFileToOpen": { 8 | "placeholder": "拖动.bilifans文件到这里", 9 | "typeValidate": "只可以上传.bilifans格式的文件" 10 | }, 11 | "chooseTemplate": "或者选择一个模版", 12 | "toggleEdit": "切换至编辑模式", 13 | "togglePreview": "切换至预览模式", 14 | "splitWindow": "拆分窗口", 15 | "mergeWindow": "合并窗口", 16 | "new": { 17 | "title": "关闭", 18 | "confirm": { 19 | "title": "关闭会丢失当前未保存的更改,是否继续?", 20 | "confirm": "继续" 21 | } 22 | }, 23 | "save": { 24 | "title": "保存", 25 | "saveImage": { 26 | "title": "保存图片", 27 | "success": "图片已保存,动态已复制" 28 | }, 29 | "export": { 30 | "title": "导出为.bilifans文件", 31 | "success": "导出成功" 32 | }, 33 | "saveTemplate": { 34 | "title": "保存为新模版", 35 | "success": "保存成功" 36 | } 37 | }, 38 | "nameDialog": { 39 | "title": "请输入模版名称", 40 | "confirm": "确定", 41 | "cancel": "取消", 42 | "empty": "模版名称不能为空" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/common/ChangelogDialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /src/layout/Sidebar/Action.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 34 | -------------------------------------------------------------------------------- /CHANGELOG_v0.md: -------------------------------------------------------------------------------- 1 | ### v0.4.0 2 | 3 | #### Features 4 | 5 | - 更换了默认背景图、默认装扮链接等,鹿野灸装扮要开售啦,[装扮链接直达](https://www.bilibili.com/h5/mall/suit/detail?navhide=1&id=106232701&native.theme=1&night=0) 6 | - 可以添加右侧渐变了 7 | - 暂时去掉了自适应缩放(因为不好用) 8 | 9 | ### v0.3.5 10 | 11 | #### Bug Fixes 12 | 修复黑暗模式下导航栏不透明问题 13 | 14 | ### v0.3.4 15 | 16 | #### Improvements 17 | 18 | - 优化pwa缓存 19 | 20 | ### v0.3.3 21 | 22 | #### Bug Fixes 23 | 24 | - 修复自适应缩放模式下,页面垂直方向遮挡问题 25 | 26 | ### v0.3.2 27 | 28 | #### Features 29 | 30 | - 菜单栏现在可以收起了 31 | 32 | ### v0.3.1 33 | 34 | #### Features 35 | 36 | - 添加自适应缩放模式(默认关闭) 37 | - 现在可以选择生成图片大小了 38 | 39 | ### v0.3.0 40 | 41 | #### Features 42 | 43 | - 添加pwa支持 44 | 45 | ### v0.2.3 46 | 47 | #### Improvements 48 | 49 | - 优化生成图片时的样式,现在生成图片不会再有一闪而过的大图 50 | 51 | ### v0.2.2 52 | 53 | #### Bug Fixes 54 | 55 | - 修复生成图片多出部分透明像素问题 56 | 57 | ### v0.2.1 58 | 59 | #### Bug Fixes 60 | 61 | - 修复生成图片分辨率低问题 62 | 63 | ### v0.2.0 64 | 65 | #### Features 66 | 67 | - 适配黑暗模式 68 | - 优化小尺寸窗口下的的样式 69 | - 优化部分设置项 70 | 71 | #### Bug Fixes 72 | 73 | - 修复当设置前景渐变色时,过渡部分颜色发黑问题 74 | 75 | ### v0.1.1 76 | 77 | #### Features 78 | 79 | - 添加更新日志 80 | - 优化界面样式 81 | 82 | ### v0.1.0 83 | 84 | - 完成基本功能 85 | -------------------------------------------------------------------------------- /src/layout/Sidebar/SidebarPanel.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 36 | -------------------------------------------------------------------------------- /src/components/ui/ResponsiveButton.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 43 | -------------------------------------------------------------------------------- /src/templates/index.ts: -------------------------------------------------------------------------------- 1 | import JSON5 from "json5"; 2 | import type { TemplateManifest } from "@/types/template"; 3 | 4 | export async function resolveDefaultTemplates() { 5 | const manifestModules = import.meta.glob("./**/manifest.jsonc", { 6 | query: "?raw", 7 | import: "default", 8 | }); 9 | 10 | const backgroundModules = import.meta.glob("./**/background.png", { 11 | import: "default", 12 | }); 13 | 14 | const templates: TemplateManifest[] = []; 15 | 16 | for (const module in manifestModules) { 17 | const dirname = module.split("/")[1]; 18 | const json = await manifestModules[module]() as string; 19 | const manifest = JSON5.parse(json); 20 | let backgroundImage: string | undefined; 21 | if (`./${dirname}/background.png` in backgroundModules) { 22 | backgroundImage = await backgroundModules[`./${dirname}/background.png`]() as string; 23 | } 24 | templates.push({ 25 | ...manifest, 26 | type: "default", 27 | cardStyle: { 28 | ...manifest.cardStyle, 29 | background: { 30 | ...manifest.cardStyle.background, 31 | image: backgroundImage, 32 | }, 33 | }, 34 | }); 35 | } 36 | 37 | return templates; 38 | } 39 | -------------------------------------------------------------------------------- /src/layout/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from "lru-cache"; 2 | import { isWebWorker } from "./is"; 3 | 4 | const cache = new LRUCache({ 5 | max: 10, 6 | }); 7 | 8 | export async function resolveImage(image: string | Blob | undefined, alt?: string): Promise { 9 | if (!image) { 10 | return; 11 | } 12 | if (image instanceof Blob) { 13 | const imageBitmap = await createImageBitmap(image); 14 | return imageBitmap; 15 | } 16 | const _isWebWorker = isWebWorker(); 17 | const key = `${image}?${_isWebWorker ? "worker" : "browser"}`; 18 | if (cache.has(key)) { 19 | return cache.get(key)!; 20 | } 21 | if (_isWebWorker) { 22 | const response = await fetch(image); 23 | const blob = await response.blob(); 24 | const imageBitmap = await createImageBitmap(blob); 25 | cache.set(key, imageBitmap); 26 | return imageBitmap; 27 | } else { 28 | const img = new Image(); 29 | // Can't set cross origin to be anonymous for data url's 30 | // https://github.com/mrdoob/three.js/issues/1305 31 | if (!image.startsWith("data")) { 32 | img.crossOrigin = "Anonymous"; 33 | } 34 | img.src = image; 35 | if (alt) { 36 | img.alt = alt; 37 | } 38 | await img.decode(); 39 | cache.set(key, img); 40 | return img; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/locale/en-US/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "light": "Light", 4 | "dark": "Dark" 5 | }, 6 | "interface": { 7 | "dragFileToOpen": { 8 | "placeholder": "Drag the .bilifans file here to open", 9 | "typeValidate": "Only .bilifans files can be uploaded" 10 | }, 11 | "chooseTemplate": "Or choose a template", 12 | "toggleEdit": "Switch to edit mode", 13 | "togglePreview": "Switch to preview mode", 14 | "splitWindow": "Split window", 15 | "mergeWindow": "Merge window", 16 | "new": { 17 | "title": "Close", 18 | "confirm": { 19 | "title": "Closing will discard the current unsaved changes, continue?", 20 | "confirm": "Continue" 21 | } 22 | }, 23 | "save": { 24 | "title": "Save", 25 | "saveImage": { 26 | "title": "Save image", 27 | "success": "Image saved successfully and post copied to clipboard" 28 | }, 29 | "export": { 30 | "title": "Export as .bilifans file", 31 | "success": "Exported successfully" 32 | }, 33 | "saveTemplate": { 34 | "title": "Save as new template", 35 | "success": "Saved successfully" 36 | } 37 | }, 38 | "nameDialog": { 39 | "title": "Please enter the template name", 40 | "confirm": "Confirm", 41 | "cancel": "Cancel", 42 | "empty": "Template name cannot be empty" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/canvas.ts: -------------------------------------------------------------------------------- 1 | export function create(width: number, height: number) { 2 | if (typeof OffscreenCanvas !== "undefined") { 3 | return createOffscreenCanvas(width, height); 4 | } 5 | return createCanvas(width, height); 6 | } 7 | 8 | export function getContext(canvas: HTMLCanvasElement | OffscreenCanvas) { 9 | const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null; 10 | if (!ctx) { 11 | throw new Error("Failed to get canvas context"); 12 | } 13 | return ctx; 14 | } 15 | 16 | export function resizeCanvas(canvas: HTMLCanvasElement | OffscreenCanvas, width: number, height: number) { 17 | if (canvas instanceof OffscreenCanvas) { 18 | canvas.width = width; 19 | canvas.height = height; 20 | return canvas; 21 | } else { 22 | const ratio = window.devicePixelRatio; 23 | canvas.width = width * ratio; 24 | canvas.height = height * ratio; 25 | return canvas; 26 | } 27 | } 28 | 29 | export function createCanvas(width: number, height: number) { 30 | const canvas = document.createElement("canvas"); 31 | resizeCanvas(canvas, width, height); 32 | canvas.style.width = `${width}px`; 33 | canvas.style.height = `${height}px`; 34 | return canvas; 35 | } 36 | 37 | export function createOffscreenCanvas(width: number, height: number) { 38 | const canvas = new OffscreenCanvas(width, height); 39 | return canvas; 40 | } 41 | -------------------------------------------------------------------------------- /src/layout/Sidebar/ActionPanel.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --header-height: 60px; 3 | --sidebar-width: 300px; 4 | --actions-width: 48px; 5 | } 6 | 7 | :root { 8 | --background: 255 255 255; 9 | --foreground: 10 10 10; 10 | --primary: 24 160 88; 11 | --primary-foreground: 250 250 250; 12 | --secondary: 245 245 245; 13 | --secondary-foreground: 23 23 23; 14 | --accent: 245 245 245; 15 | --accent-foreground: 23 23 23; 16 | --muted: 245 245 245; 17 | --muted-foreground: 115 115 115; 18 | --border: 229 231 235; 19 | --info: 0 179 240; 20 | --info-foreground: 255 255 255; 21 | --success: 0 169 111; 22 | --success-foreground: 255 255 255; 23 | --warning: 255 194 45; 24 | --warning-foreground: 0 0 0; 25 | --error: 255 111 112; 26 | --error-foreground: 255 255 255; 27 | --radius: 0.5rem; 28 | } 29 | 30 | .dark { 31 | --background: 10 10 10; 32 | --foreground: 250 250 250; 33 | --primary: 23 152 83; 34 | --primary-foreground: 250 250 250; 35 | --secondary: 38 38 38; 36 | --secondary-foreground: 250 250 250; 37 | --accent: 38 38 38; 38 | --accent-foreground: 250 250 250; 39 | --muted: 38 38 38; 40 | --muted-foreground: 250 250 250; 41 | --border: 41 37 36; 42 | --info: 0 179 240; 43 | --info-foreground: 255 255 255; 44 | --success: 0 169 111; 45 | --success-foreground: 255 255 255; 46 | --warning: 255 194 45; 47 | --warning-foreground: 0 0 0; 48 | --error: 255 111 112; 49 | --error-foreground: 255 255 255; 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import { enUS } from "@/locale/en-US"; 2 | import { zhCN } from "@/locale/zh-CN"; 3 | import { n } from "@bernankez/utils"; 4 | import { createI18n } from "vue-i18n"; 5 | 6 | export const availableLocales = n(["en-US", "zh-CN"]); 7 | export type AvailableLocales = typeof availableLocales[number]; 8 | export const defaultLocale = "zh-CN"; 9 | export const fallbackLocale = "en-US"; 10 | 11 | export const i18n = createI18n({ 12 | legacy: false, 13 | locale: defaultLocale, 14 | fallbackLocale, 15 | messages: { 16 | "zh-CN": zhCN, 17 | "en-US": enUS, 18 | }, 19 | }); 20 | 21 | export async function setLocale(locale: AvailableLocales) { 22 | i18n.global.locale.value = locale; 23 | const appStore = useAppStore(); 24 | appStore.locale = locale; 25 | document.querySelector("html")?.setAttribute("lang", locale); 26 | } 27 | 28 | export function inferPreferredLocale(): AvailableLocales { 29 | const appStore = useAppStore(); 30 | if (appStore.locale) { 31 | return appStore.locale; 32 | } 33 | const userLocale = (window.navigator.language || defaultLocale) as AvailableLocales; 34 | if (availableLocales.indexOf(userLocale) > 0) { 35 | return userLocale; 36 | } 37 | const userLocaleWithoutRegion = userLocale.split("-")[0] as AvailableLocales; 38 | const availableLocale = availableLocales.find(locale => locale.startsWith(userLocaleWithoutRegion)); 39 | if (availableLocale) { 40 | return availableLocale; 41 | } 42 | return defaultLocale; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/common/PreviewPost.vue: -------------------------------------------------------------------------------- 1 | 2 | 48 | 49 | 54 | -------------------------------------------------------------------------------- /src/components/ui/Button.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 48 | -------------------------------------------------------------------------------- /src/composables/usePalette.ts: -------------------------------------------------------------------------------- 1 | import { palette as _palette } from "@/workers/palette"; 2 | import type { Palette } from "@/utils/rgbaster"; 3 | import type { MaybeRefOrGetter } from "vue"; 4 | 5 | export function usePalette(image: MaybeRefOrGetter, count = 3) { 6 | const palette = ref([]); 7 | 8 | const { url } = useBlobUrl(image); 9 | 10 | watch(url, async (url) => { 11 | if (url) { 12 | const img = new Image(); 13 | if (!url.startsWith("data")) { 14 | img.crossOrigin = "Anonymous"; 15 | } 16 | img.src = url; 17 | await img.decode(); 18 | const result = await _palette(url, img.width, img.height); 19 | palette.value = result.slice(0, count).map(({ color, count }) => ({ 20 | color: rgb2hex(color), 21 | count, 22 | })); 23 | } else { 24 | palette.value = []; 25 | } 26 | }, { immediate: true }); 27 | 28 | return { palette }; 29 | } 30 | 31 | function rgb2hex(text: string) { 32 | const [r, g, b] = text.match(/\d+/g)!.map(Number); 33 | return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}ff`; 34 | } 35 | 36 | // #ref: https://juejin.cn/post/6844903511956815885 37 | export function inferFontColor(hex: string) { 38 | const colors = hex.match(/\w\w/g)!.map(String).map(v => Number.parseInt(v, 16)); 39 | const grayLevel = Number(colors[0]) * 0.299 + Number(colors[1]) * 0.587 + Number(colors[2]) * 0.114; 40 | if (grayLevel >= 192) { 41 | return "#333333"; 42 | } else { 43 | return "#ffffff"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/common/NameDialog.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 59 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetIcons, presetTypography, presetUno, transformerDirectives } from "unocss"; 2 | 3 | export default defineConfig({ 4 | presets: [presetUno(), presetIcons(), presetTypography()], 5 | transformers: [transformerDirectives()], 6 | theme: { 7 | colors: { 8 | background: "rgb(var(--background))", 9 | foreground: "rgb(var(--foreground))", 10 | primary: { 11 | DEFAULT: "rgb(var(--primary))", 12 | foreground: "rgb(var(--primary-foreground))", 13 | }, 14 | secondary: { 15 | DEFAULT: "rgb(var(--secondary))", 16 | foreground: "rgb(var(--secondary-foreground))", 17 | }, 18 | accent: { 19 | DEFAULT: "rgb(var(--accent))", 20 | foreground: "rgb(var(--accent-foreground))", 21 | }, 22 | muted: { 23 | DEFAULT: "rgb(var(--muted))", 24 | foreground: "rgb(var(--muted-foreground))", 25 | }, 26 | border: "rgb(var(--border))", 27 | info: { 28 | DEFAULT: "rgb(var(--info))", 29 | foreground: "rgb(var(--info-foreground))", 30 | }, 31 | success: { 32 | DEFAULT: "rgb(var(--success))", 33 | foreground: "rgb(var(--success-foreground))", 34 | }, 35 | warning: { 36 | DEFAULT: "rgb(var(--warning))", 37 | foreground: "rgb(var(--warning-foreground))", 38 | }, 39 | error: { 40 | DEFAULT: "rgb(var(--error))", 41 | foreground: "rgb(var(--error-foreground))", 42 | }, 43 | }, 44 | borderRadius: { 45 | lg: "var(--radius)", 46 | md: "calc(var(--radius) - 2px)", 47 | sm: "calc(var(--radius) - 4px)", 48 | }, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/ui/CropperToolbox.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bilibili-fans", 3 | "type": "module", 4 | "version": "1.1.1", 5 | "private": true, 6 | "packageManager": "pnpm@9.10.0", 7 | "scripts": { 8 | "prepare": "simple-git-hooks", 9 | "dev": "vite", 10 | "build": "pnpm typecheck && vite build", 11 | "release": "bumpp", 12 | "typecheck": "vue-tsc -b", 13 | "preview": "vite preview", 14 | "lint": "eslint . -f mo", 15 | "fix": "eslint . -f mo --fix" 16 | }, 17 | "dependencies": { 18 | "@bernankez/utils": "^0.6.4", 19 | "@unocss/reset": "^0.62.3", 20 | "@vueuse/core": "^11.0.3", 21 | "@vueuse/integrations": "^11.0.3", 22 | "clsx": "^2.1.1", 23 | "compressorjs": "^1.2.1", 24 | "dayjs": "^1.11.13", 25 | "idb-keyval": "^6.2.1", 26 | "json5": "^2.2.3", 27 | "jszip": "^3.10.1", 28 | "klona": "^2.0.6", 29 | "lru-cache": "^11.0.1", 30 | "nanoid": "^5.0.7", 31 | "pinia": "^2.2.2", 32 | "pinia-plugin-persistedstate": "^4.0.1", 33 | "unocss": "^0.62.3", 34 | "unplugin-vue-markdown": "^0.26.2", 35 | "vue": "^3.5.5", 36 | "vue-advanced-cropper": "^2.8.9", 37 | "vue-i18n": "^10.0.1", 38 | "vue-router": "^4.4.5" 39 | }, 40 | "devDependencies": { 41 | "@bernankez/eslint-config": "^2.2.0", 42 | "@iconify-json/fluent-emoji": "^1.2.0", 43 | "@iconify-json/fluent-emoji-flat": "^1.2.0", 44 | "@iconify-json/material-symbols": "^1.2.1", 45 | "@iconify-json/uil": "^1.2.0", 46 | "@types/node": "^22.5.5", 47 | "@unocss/eslint-plugin": "^0.62.3", 48 | "@vitejs/plugin-vue": "^5.1.3", 49 | "@vitejs/plugin-vue-jsx": "^4.0.1", 50 | "@vueuse/shared": "^11.0.3", 51 | "bumpp": "^9.5.2", 52 | "eslint": "^9.10.0", 53 | "eslint-formatter-mo": "^1.2.0", 54 | "eslint-plugin-format": "^0.1.2", 55 | "lint-staged": "^15.2.10", 56 | "naive-ui": "^2.39.0", 57 | "simple-git-hooks": "^2.11.1", 58 | "typescript": "^5.6.2", 59 | "unplugin-auto-import": "^0.18.3", 60 | "unplugin-vue-components": "^0.27.4", 61 | "v-lazy-show": "^0.2.4", 62 | "vite": "^5.4.5", 63 | "vite-plugin-font-carrier": "^0.1.4", 64 | "vite-plugin-pwa": "^0.20.5", 65 | "vite-plugin-vue-devtools": "^7.4.5", 66 | "vue-tsc": "^2.1.6" 67 | }, 68 | "simple-git-hooks": { 69 | "pre-commit": "pnpm lint-staged" 70 | }, 71 | "lint-staged": { 72 | "*": [ 73 | "eslint --fix" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/template.ts: -------------------------------------------------------------------------------- 1 | import JSON5 from "json5"; 2 | import JSZip from "jszip"; 3 | import { nanoid } from "nanoid"; 4 | import type { TemplateManifest } from "@/types/template"; 5 | 6 | export const accept = ".bilifans"; 7 | 8 | export async function importTemplate(file: File): Promise | undefined> { 9 | const zip = new JSZip(); 10 | const zipFile = await zip.loadAsync(file); 11 | const manifest = zipFile.file("manifest.jsonc"); 12 | const background = zipFile.file("background.png"); 13 | if (manifest) { 14 | const manifestContent = await manifest.async("string"); 15 | const manifestJson = JSON5.parse(manifestContent); 16 | const backgroundBlob = background ? await background.async("blob") : undefined; 17 | const template: TemplateManifest = { 18 | ...manifestJson, 19 | id: nanoid(), 20 | type: "custom", 21 | cardStyle: { 22 | ...manifestJson.cardStyle, 23 | background: { 24 | ...manifestJson.cardStyle.background, 25 | image: backgroundBlob, 26 | }, 27 | }, 28 | }; 29 | return template; 30 | } 31 | } 32 | 33 | export async function exportTemplate(template: TemplateManifest): Promise { 34 | const zip = new JSZip(); 35 | const { type, id, ...rest } = template; 36 | const targetTemplate: Omit, "type" | "id"> = { 37 | ...rest, 38 | cardStyle: { 39 | ...rest.cardStyle, 40 | background: { 41 | ...rest.cardStyle.background, 42 | image: undefined, 43 | }, 44 | }, 45 | }; 46 | zip.file("manifest.jsonc", JSON5.stringify(targetTemplate, null, 2)); 47 | if (template.cardStyle.background.image) { 48 | zip.file("background.png", new File([await imageToBlob(template.cardStyle.background.image)], "background.png")); 49 | } 50 | const blob = await zip.generateAsync({ type: "blob" }); 51 | return blob; 52 | } 53 | 54 | export async function imageToBlob(image: string | Blob) { 55 | if (typeof image === "string") { 56 | const img = document.createElement("img"); 57 | img.src = image; 58 | await img.decode(); 59 | const canvas = document.createElement("canvas"); 60 | canvas.width = img.width; 61 | canvas.height = img.height; 62 | const ctx = canvas.getContext("2d")!; 63 | ctx.drawImage(img, 0, 0); 64 | const blob = await new Promise((resolve, reject) => { 65 | canvas.toBlob((blob) => { 66 | if (blob) { 67 | resolve(blob); 68 | } 69 | reject(new Error("Failed to convert image to blob")); 70 | }); 71 | }); 72 | return blob; 73 | } 74 | return image; 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/rgbaster.ts: -------------------------------------------------------------------------------- 1 | import { resolveImage } from "./cache"; 2 | import { create, getContext } from "./canvas"; 3 | 4 | export interface PaletteOptions { 5 | width: number; 6 | height: number; 7 | ignore?: string[]; 8 | scale?: number; 9 | skipTransparentPixels?: boolean; 10 | } 11 | 12 | export interface Palette { 13 | color: string; 14 | count: number; 15 | } 16 | 17 | export async function analyze(src: Blob | string, options: PaletteOptions): Promise { 18 | const { 19 | ignore = [], // for example, to ignore white and black: [ 'rgb(0,0,0)', 'rgb(255,255,255)' ] 20 | scale = 1, // 0 = best performance, lowest fidelity 21 | // 1 = best fidelity, worst performance 22 | } = options; 23 | 24 | if (scale > 1 || scale <= 0) { 25 | console.warn(`You set scale to ${scale}, which isn't between 0-1. This is either pointless (> 1) or a no-op (≤ 0)`); 26 | } 27 | 28 | const width = options.width * scale; 29 | const height = options.height * scale; 30 | 31 | const data = await getImageData(src, { width, height }); 32 | return getCounts(data, ignore); 33 | } 34 | 35 | export async function getImageData(src: Blob | string, options: { width: number;height: number }): Promise { 36 | const { width, height } = options; 37 | const canvas = create(width, height); 38 | const ctx = getContext(canvas); 39 | const img = (await resolveImage(src))!; 40 | ctx.drawImage(img, 0, 0, width, height); 41 | const { data } = ctx.getImageData(0, 0, width, height); 42 | return data; 43 | } 44 | 45 | export function getCounts(data: Uint8ClampedArray, ignore: string[]): Palette[] { 46 | const countMap: Record = {}; 47 | 48 | for (let i = 0; i < data.length; i += 4 /* 4 gives us r, g, b, and a */) { 49 | const alpha: number = data[i + 3]; 50 | // skip FULLY transparent pixels 51 | if (alpha === 0) { 52 | continue; 53 | } 54 | 55 | const rgbComponents: (number | undefined)[] = Array.from(data.subarray(i, i + 3)); 56 | 57 | // skip undefined data 58 | if (rgbComponents.includes(undefined)) { 59 | continue; 60 | } 61 | 62 | const color: string = alpha && alpha !== 255 63 | ? `rgba(${[...rgbComponents, alpha].join(",")})` 64 | : `rgb(${rgbComponents.join(",")})`; 65 | 66 | // skip colors in the ignore list 67 | if (ignore.includes(color)) { 68 | continue; 69 | } 70 | 71 | if (countMap[color]) { 72 | countMap[color].count++; 73 | } else { 74 | countMap[color] = { color, count: 1 }; 75 | } 76 | } 77 | 78 | const counts = Object.values(countMap) as Palette[]; 79 | return counts.sort((a, b) => b.count - a.count); 80 | } 81 | -------------------------------------------------------------------------------- /src/store/template.ts: -------------------------------------------------------------------------------- 1 | import { resolveDefaultTemplates } from "@/templates"; 2 | import { imageToBlob } from "@/utils/template"; 3 | import { useIDBKeyval } from "@vueuse/integrations/useIDBKeyval"; 4 | import { klona } from "klona"; 5 | import { nanoid } from "nanoid"; 6 | import { defineStore } from "pinia"; 7 | import type { TemplateManifest } from "@/types/template"; 8 | /** 9 | * @see https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189 10 | */ 11 | // eslint-disable-next-line unused-imports/no-unused-imports 12 | import type { RemovableRef } from "@vueuse/shared"; 13 | 14 | export const useTemplateStore = defineStore("template", () => { 15 | const defaultTemplates = ref[]>([]); 16 | const { data: customTemplates, isFinished: resolvingCustomTemplates } = useIDBKeyval[]>("bilifans-custom-template", []); 17 | const { data: currentTemplate, isFinished: resolvingCurrentTemplate } = useIDBKeyval | undefined>("bilifans-current-template", undefined); 18 | const resolvingDefaultTemplates = ref(true); 19 | const loading = computed(() => !resolvingCustomTemplates.value || resolvingDefaultTemplates.value || !resolvingCurrentTemplate.value); 20 | 21 | resolveDefaultTemplates().then((templates) => { 22 | defaultTemplates.value = templates; 23 | resolvingDefaultTemplates.value = false; 24 | }); 25 | 26 | function loadTemplate(id: string) { 27 | if (currentTemplate.value?.id === id) { 28 | return; 29 | } 30 | 31 | const stop = watchEffect(() => { 32 | if (!loading.value) { 33 | nextTick(() => stop()); 34 | setTemplate(); 35 | } 36 | }); 37 | 38 | function setTemplate() { 39 | const _defaultTemplate = defaultTemplates.value.find(item => item.id === id); 40 | const _customTemplate = customTemplates.value.find(item => item.id === id); 41 | if (_defaultTemplate) { 42 | currentTemplate.value = klona(_defaultTemplate); 43 | } else if (_customTemplate) { 44 | currentTemplate.value = klona(_customTemplate); 45 | } 46 | } 47 | } 48 | 49 | async function addCustomTemplate(template: Omit, "type" | "id">) { 50 | const image = template.cardStyle.background.image; 51 | const backgroundImage = image ? await imageToBlob(image) : undefined; 52 | const targetTemplate: TemplateManifest = { 53 | ...template, 54 | type: "custom", 55 | id: nanoid(), 56 | cardStyle: { 57 | ...template.cardStyle, 58 | background: { 59 | ...template.cardStyle.background, 60 | image: backgroundImage, 61 | }, 62 | }, 63 | }; 64 | customTemplates.value.push(klona(targetTemplate)); 65 | } 66 | 67 | function removeCustomTemplate(id: string) { 68 | const index = customTemplates.value.findIndex(t => t.id === id); 69 | customTemplates.value.splice(index, 1); 70 | } 71 | 72 | return { 73 | defaultTemplates, 74 | customTemplates, 75 | currentTemplate, 76 | loading, 77 | 78 | loadTemplate, 79 | addCustomTemplate, 80 | removeCustomTemplate, 81 | }; 82 | }); 83 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 21 | 33 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/layout/Sidebar/Copywriting.vue: -------------------------------------------------------------------------------- 1 | 2 | 37 | 38 | 77 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ActionFormItem: typeof import('./src/components/ui/ActionFormItem.vue')['default'] 11 | AvatarCropper: typeof import('./src/components/common/AvatarCropper.vue')['default'] 12 | AvatarCropperDialog: typeof import('./src/components/common/AvatarCropperDialog.vue')['default'] 13 | Button: typeof import('./src/components/ui/Button.vue')['default'] 14 | ChangelogDialog: typeof import('./src/components/common/ChangelogDialog.vue')['default'] 15 | ConfigProvider: typeof import('./src/components/global/ConfigProvider.vue')['default'] 16 | Cropper: typeof import('./src/components/global/Cropper.vue')['default'] 17 | CropperToolbox: typeof import('./src/components/ui/CropperToolbox.vue')['default'] 18 | DemoFansCard: typeof import('./src/components/common/DemoFansCard.vue')['default'] 19 | FansCard: typeof import('./src/components/common/FansCard.vue')['default'] 20 | NameDialog: typeof import('./src/components/common/NameDialog.vue')['default'] 21 | NButton: typeof import('naive-ui')['NButton'] 22 | NCheckbox: typeof import('naive-ui')['NCheckbox'] 23 | NCollapse: typeof import('naive-ui')['NCollapse'] 24 | NCollapseItem: typeof import('naive-ui')['NCollapseItem'] 25 | NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] 26 | NColorPicker: typeof import('naive-ui')['NColorPicker'] 27 | NConfigProvider: typeof import('naive-ui')['NConfigProvider'] 28 | NDatePicker: typeof import('naive-ui')['NDatePicker'] 29 | NDialogProvider: typeof import('naive-ui')['NDialogProvider'] 30 | NDivider: typeof import('naive-ui')['NDivider'] 31 | NDropdown: typeof import('naive-ui')['NDropdown'] 32 | NForm: typeof import('naive-ui')['NForm'] 33 | NFormItem: typeof import('naive-ui')['NFormItem'] 34 | NH2: typeof import('naive-ui')['NH2'] 35 | NInput: typeof import('naive-ui')['NInput'] 36 | NInputNumber: typeof import('naive-ui')['NInputNumber'] 37 | NMessageProvider: typeof import('naive-ui')['NMessageProvider'] 38 | NModal: typeof import('naive-ui')['NModal'] 39 | NPopconfirm: typeof import('naive-ui')['NPopconfirm'] 40 | NSelect: typeof import('naive-ui')['NSelect'] 41 | NSlider: typeof import('naive-ui')['NSlider'] 42 | NSplit: typeof import('naive-ui')['NSplit'] 43 | NSwitch: typeof import('naive-ui')['NSwitch'] 44 | NText: typeof import('naive-ui')['NText'] 45 | NTooltip: typeof import('naive-ui')['NTooltip'] 46 | NUpload: typeof import('naive-ui')['NUpload'] 47 | NUploadDragger: typeof import('naive-ui')['NUploadDragger'] 48 | Palette: typeof import('./src/components/common/Palette.vue')['default'] 49 | PreviewPost: typeof import('./src/components/common/PreviewPost.vue')['default'] 50 | ResponsiveButton: typeof import('./src/components/ui/ResponsiveButton.vue')['default'] 51 | RouterLink: typeof import('vue-router')['RouterLink'] 52 | RouterView: typeof import('vue-router')['RouterView'] 53 | WelcomeFansCard: typeof import('./src/components/common/WelcomeFansCard.vue')['default'] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/common/FansCard.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 130 | -------------------------------------------------------------------------------- /src/layout/Sidebar/User.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 109 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path"; 2 | import { UtilsResolver } from "@bernankez/utils/resolver"; 3 | import vue from "@vitejs/plugin-vue"; 4 | import vueJsx from "@vitejs/plugin-vue-jsx"; 5 | import UnoCSS from "unocss/vite"; 6 | import AutoImport from "unplugin-auto-import/vite"; 7 | import { NaiveUiResolver } from "unplugin-vue-components/resolvers"; 8 | import Components from "unplugin-vue-components/vite"; 9 | import Markdown from "unplugin-vue-markdown/vite"; 10 | import { transformLazyShow } from "v-lazy-show"; 11 | import { defineConfig } from "vite"; 12 | import FontCarrier, { numberChars } from "vite-plugin-font-carrier"; 13 | import { VitePWA } from "vite-plugin-pwa"; 14 | import VueDevTools from "vite-plugin-vue-devtools"; 15 | 16 | function createFontCarrier() { 17 | return FontCarrier({ 18 | fonts: [ 19 | { 20 | path: "./src/assets/font/Kenney-Mini-Square.ttf", 21 | input: numberChars, 22 | }, 23 | ], 24 | type: "woff2", 25 | }); 26 | } 27 | 28 | // https://vitejs.dev/config/ 29 | export default defineConfig(() => { 30 | return { 31 | plugins: [ 32 | vue({ 33 | include: [/\.vue$/, /\.tsx$/, /\.md$/], 34 | template: { 35 | compilerOptions: { 36 | nodeTransforms: [ 37 | transformLazyShow, 38 | ], 39 | }, 40 | }, 41 | }), 42 | vueJsx(), 43 | UnoCSS(), 44 | Markdown({ 45 | markdownItOptions: { 46 | html: true, 47 | linkify: true, 48 | typographer: true, 49 | }, 50 | }), 51 | AutoImport({ 52 | imports: [ 53 | "vue", 54 | "vue-i18n", 55 | "vue-router", 56 | "@vueuse/core", 57 | "pinia", 58 | { 59 | "naive-ui": [ 60 | "useDialog", 61 | "useMessage", 62 | "useNotification", 63 | "useLoadingBar", 64 | ], 65 | }, 66 | ], 67 | dirs: ["./src/composables/**", "./src/store/**"], 68 | vueTemplate: true, 69 | resolvers: [UtilsResolver()], 70 | }), 71 | Components({ 72 | dirs: ["./src/components/**"], 73 | resolvers: [NaiveUiResolver()], 74 | }), 75 | createFontCarrier(), 76 | VitePWA({ 77 | registerType: "autoUpdate", 78 | includeAssets: ["favicon.ico", "apple-touch-icon.png", "masked-icon.svg"], 79 | manifest: { 80 | name: "BilibliFans", 81 | short_name: "BilibliFans", 82 | description: "A tool making bilibili fans card", 83 | theme_color: "#18a058", 84 | background_color: "#ffffff", 85 | display: "standalone", 86 | icons: [ 87 | { 88 | src: "/favicon-32x32.png", 89 | sizes: "32x32", 90 | type: "image/png", 91 | }, 92 | { 93 | src: "/android-chrome-192x192.png", 94 | sizes: "192x192", 95 | type: "image/png", 96 | }, 97 | { 98 | src: "/android-chrome-512x512.png", 99 | sizes: "512x512", 100 | type: "image/png", 101 | purpose: "any", 102 | }, 103 | { 104 | src: "/pwa-512x512.png", 105 | sizes: "512x512", 106 | type: "image/png", 107 | purpose: "maskable", 108 | }, 109 | ], 110 | }, 111 | workbox: { 112 | globPatterns: ["**/*.{js,css,html,png,jpg,svg,ttf,woff2}"], 113 | }, 114 | devOptions: { 115 | enabled: true, 116 | }, 117 | }), 118 | VueDevTools(), 119 | ], 120 | worker: { 121 | format: "es" as const, 122 | plugins: () => [createFontCarrier()], 123 | }, 124 | resolve: { 125 | alias: { 126 | "@": resolve(__dirname, "src"), 127 | "~": resolve(__dirname, "."), 128 | }, 129 | }, 130 | }; 131 | }); 132 | -------------------------------------------------------------------------------- /src/layout/Sidebar/Template.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 126 | -------------------------------------------------------------------------------- /src/components/common/AvatarCropperDialog.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 135 | 136 | 143 | -------------------------------------------------------------------------------- /src/layout/Sidebar/Setting.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 163 | -------------------------------------------------------------------------------- /src/layout/Sidebar/Card.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 133 | -------------------------------------------------------------------------------- /src/layout/Header.vue: -------------------------------------------------------------------------------- 1 | 138 | 139 | 173 | -------------------------------------------------------------------------------- /src/components/global/Cropper.vue: -------------------------------------------------------------------------------- 1 | 214 | 215 | 228 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 164 | 165 | 180 | -------------------------------------------------------------------------------- /src/utils/draw.ts: -------------------------------------------------------------------------------- 1 | import KenneyMini from "@/assets/font/Kenney-Mini-Square.ttf"; 2 | import { isDefined } from "@bernankez/utils"; 3 | import Compressor from "compressorjs"; 4 | import type { User } from "@/types/user"; 5 | import { resolveImage } from "./cache"; 6 | import { create, getContext } from "./canvas"; 7 | import { isWebWorker } from "./is"; 8 | 9 | export interface RawDrawOptions { 10 | width: number; 11 | height: number; 12 | template?: { 13 | cardStyle: { 14 | color?: string; 15 | foreground?: { 16 | gradient: { 17 | [K in "left" | "right"]?: { 18 | start: number; 19 | end: number; 20 | color: string; 21 | }; 22 | }; 23 | }; 24 | background?: { 25 | image?: string | Blob; 26 | origin?: [number, number]; 27 | size?: [number, number]; 28 | color?: string; 29 | }; 30 | }; 31 | }; 32 | user?: Partial; 33 | } 34 | 35 | export interface DrawOptions { 36 | width: number; 37 | height: number; 38 | fontSize: number; 39 | cardStyle: { 40 | color: string; 41 | borderRadius: number; 42 | font: { 43 | normal: { 44 | fontFamily: string; 45 | }; 46 | mono: { 47 | fontFamily: string; 48 | opacity: number; 49 | }; 50 | kenney: { 51 | fontFamily: string; 52 | }; 53 | }; 54 | padding: { 55 | top: number; 56 | right: number; 57 | bottom: number; 58 | left: number; 59 | }; 60 | foreground?: { 61 | gradient: { 62 | [K in "left" | "right"]?: { 63 | color: string; 64 | start: number; 65 | end: number; 66 | }; 67 | }; 68 | }; 69 | background?: { 70 | image?: ImageBitmap | HTMLImageElement; 71 | origin: [number, number]; 72 | size: [number, number]; 73 | color?: string; 74 | }; 75 | }; 76 | user: { 77 | nickname?: { 78 | text: string; 79 | style: { 80 | fontSize: number; 81 | x: number; 82 | y: number; 83 | }; 84 | }; 85 | avatar?: { 86 | image: ImageBitmap | HTMLImageElement; 87 | style: { 88 | width: number; 89 | height: number; 90 | outline: { 91 | color: string; 92 | width: number; 93 | }; 94 | x: number; 95 | y: number; 96 | }; 97 | }; 98 | no: { 99 | title: { 100 | text: string; 101 | style: { 102 | fontSize: number; 103 | x: number; 104 | y: number; 105 | }; 106 | }; 107 | number?: { 108 | text: string; 109 | style: { 110 | fontSize: number; 111 | letterSpacing: number; 112 | x: number; 113 | y: number; 114 | }; 115 | }; 116 | }; 117 | date: { 118 | title: { 119 | text: string; 120 | style: { 121 | fontSize: number; 122 | x: number; 123 | y: number; 124 | }; 125 | }; 126 | date?: { 127 | text: string; 128 | style: { 129 | fontSize: number; 130 | letterSpacing: number; 131 | x: number; 132 | y: number; 133 | }; 134 | }; 135 | }; 136 | }; 137 | } 138 | 139 | export async function resolveOptions(options: RawDrawOptions): Promise { 140 | const { template, user, width, height } = options; 141 | const { cardStyle } = template || {}; 142 | const { background } = cardStyle || {}; 143 | 144 | const backgroundImage = background && await resolveImage(background.image); 145 | const avatarImage = user && await resolveImage(user.avatar); 146 | 147 | const fontSize = width / 30.375; 148 | const padding: DrawOptions["cardStyle"]["padding"] = { 149 | top: 0.875 * fontSize, 150 | right: 1.125 * fontSize, 151 | bottom: 0.4375 * fontSize, 152 | left: 1.125 * fontSize, 153 | }; 154 | const _avatarStyle = { 155 | width: 1.9375 * fontSize, 156 | height: 1.9375 * fontSize, 157 | outline: { 158 | color: "#fff", 159 | width: 0.0625 * fontSize, 160 | }, 161 | x: padding.left, 162 | y: padding.top, 163 | }; 164 | const avatar: DrawOptions["user"]["avatar"] = avatarImage 165 | ? { 166 | image: avatarImage, 167 | style: _avatarStyle, 168 | } 169 | : undefined; 170 | const nickname: DrawOptions["user"]["nickname"] = user?.nickname 171 | ? { 172 | text: user.nickname, 173 | style: { 174 | fontSize: 1.125 * fontSize, 175 | x: padding.left + _avatarStyle.width + 0.8 * fontSize, 176 | y: padding.top + 1.87 * fontSize, 177 | }, 178 | } 179 | : undefined; 180 | const no: DrawOptions["user"]["no"] = { 181 | title: { 182 | text: "FANS NO.", 183 | style: { 184 | fontSize: 1 * fontSize, 185 | x: padding.left, 186 | y: padding.top + 4.825 * fontSize, 187 | }, 188 | }, 189 | number: isDefined(user?.no) 190 | ? { 191 | text: user.no.toString().padStart(6, "0"), 192 | style: { 193 | fontSize: 1.8125 * fontSize, 194 | letterSpacing: 0.0625 * fontSize, 195 | x: padding.left, 196 | y: padding.top + 7.15 * fontSize, 197 | }, 198 | } 199 | : undefined, 200 | }; 201 | const date: DrawOptions["user"]["date"] = { 202 | title: { 203 | text: "DATE", 204 | style: { 205 | fontSize: 1 * fontSize, 206 | x: padding.left, 207 | y: padding.top + 9.35 * fontSize, 208 | }, 209 | }, 210 | date: isDefined(user?.date) 211 | ? { 212 | text: user.date, 213 | style: { 214 | fontSize: 1 * fontSize, 215 | letterSpacing: 0.04 * fontSize, 216 | x: padding.left, 217 | y: padding.top + 10.725 * fontSize, 218 | }, 219 | } 220 | : undefined, 221 | }; 222 | 223 | return { 224 | width, 225 | height, 226 | fontSize, 227 | cardStyle: { 228 | color: cardStyle?.color || "#ffffff", 229 | borderRadius: fontSize * 0.375, 230 | font: { 231 | normal: { 232 | fontFamily: "Avenir, system-ui, -apple-system, \"Segoe UI\", Roboto, Emoji, Helvetica, Arial, sans-serif", 233 | }, 234 | mono: { 235 | fontFamily: "\"Google Sans Text\", Arial, Helvetica, sans-serif", 236 | opacity: 0.5, 237 | }, 238 | kenney: { 239 | fontFamily: "kenney mini", 240 | }, 241 | }, 242 | padding, 243 | foreground: cardStyle?.foreground, 244 | background: { 245 | image: backgroundImage, 246 | origin: background?.origin || [0, 0], 247 | size: background?.size || [width, height], 248 | color: background?.color, 249 | }, 250 | }, 251 | user: { 252 | avatar, 253 | nickname, 254 | no, 255 | date, 256 | }, 257 | }; 258 | } 259 | 260 | export async function loadFont() { 261 | const kenney = "kenney mini"; 262 | const _isWebWorker = isWebWorker(); 263 | const fonts = _isWebWorker ? globalThis.fonts : document.fonts; 264 | for (const font of fonts) { 265 | if (font.family === kenney) { 266 | return; 267 | } 268 | } 269 | let font: FontFace; 270 | if (_isWebWorker) { 271 | const response = await fetch(KenneyMini); 272 | const blob = await response.blob(); 273 | const buffer = await blob.arrayBuffer(); 274 | font = new FontFace(kenney, buffer); 275 | } else { 276 | font = new FontFace(kenney, KenneyMini); 277 | } 278 | await font.load(); 279 | fonts.add(font); 280 | } 281 | 282 | export async function render(options: RawDrawOptions, canvas?: HTMLCanvasElement | OffscreenCanvas) { 283 | const drawOptions = await resolveOptions(options); 284 | await loadFont(); 285 | const { width, height, cardStyle } = drawOptions; 286 | const _canvas = canvas || create(width, height); 287 | const ctx = getContext(_canvas); 288 | clipBackground(ctx, drawOptions); 289 | if (cardStyle.background) { 290 | const { image, origin, size, color: backgroundColor } = cardStyle.background; 291 | if (backgroundColor) { 292 | // Background color 293 | ctx.save(); 294 | ctx.fillStyle = backgroundColor; 295 | ctx.fill(); 296 | ctx.restore(); 297 | } 298 | // Background image 299 | if (image) { 300 | ctx.drawImage(image, origin[0], origin[1], size[0], size[1], 0, 0, width, height); 301 | } 302 | } 303 | if (cardStyle.foreground) { 304 | drawForeground(ctx, drawOptions); 305 | } 306 | drawUser(ctx, drawOptions); 307 | drawFansNo(ctx, drawOptions); 308 | drawDate(ctx, drawOptions); 309 | return _canvas; 310 | } 311 | 312 | export function clipBackground(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, options: DrawOptions) { 313 | const { width, height, cardStyle } = options; 314 | const { borderRadius } = cardStyle; 315 | ctx.beginPath(); 316 | ctx.moveTo(borderRadius, 0); 317 | ctx.lineTo(width - borderRadius, 0); 318 | ctx.quadraticCurveTo(width, 0, width, borderRadius); 319 | ctx.lineTo(width, height - borderRadius); 320 | ctx.quadraticCurveTo(width, height, width - borderRadius, height); 321 | ctx.lineTo(borderRadius, height); 322 | ctx.quadraticCurveTo(0, height, 0, height - borderRadius); 323 | ctx.lineTo(0, borderRadius); 324 | ctx.quadraticCurveTo(0, 0, borderRadius, 0); 325 | ctx.closePath(); 326 | ctx.clip(); 327 | } 328 | 329 | export function drawForeground(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, options: DrawOptions) { 330 | const { width, height, cardStyle } = options; 331 | if (cardStyle.foreground) { 332 | const { gradient } = cardStyle.foreground || {}; 333 | const { left, right } = gradient || {}; 334 | 335 | ctx.save(); 336 | if (left) { 337 | const { color, start, end } = left; 338 | const gradient = ctx.createLinearGradient(0, 0, width, 0); 339 | gradient.addColorStop(0, color); 340 | gradient.addColorStop(start, color); 341 | gradient.addColorStop(end, "#ffffff00"); 342 | gradient.addColorStop(1, "#ffffff00"); 343 | ctx.fillStyle = gradient; 344 | ctx.fillRect(0, 0, width, height); 345 | } 346 | 347 | if (right) { 348 | const { color, start, end } = right; 349 | const gradient = ctx.createLinearGradient(width, 0, 0, 0); 350 | gradient.addColorStop(0, color); 351 | gradient.addColorStop(start, color); 352 | gradient.addColorStop(end, "#ffffff00"); 353 | gradient.addColorStop(1, "#ffffff00"); 354 | ctx.fillStyle = gradient; 355 | ctx.fillRect(0, 0, width, height); 356 | } 357 | ctx.restore(); 358 | } 359 | } 360 | 361 | export function drawUser(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, options: DrawOptions) { 362 | const { cardStyle, user } = options; 363 | const { color, padding, font } = cardStyle; 364 | const { avatar, nickname } = user; 365 | if (avatar) { 366 | const { outline } = avatar.style; 367 | // outline 368 | ctx.save(); 369 | ctx.beginPath(); 370 | ctx.arc(padding.left + avatar.style.width / 2, padding.top + avatar.style.height / 2, avatar.style.width / 2 + outline.width, 0, Math.PI * 2); 371 | ctx.strokeStyle = outline.color; 372 | ctx.lineWidth = outline.width; 373 | ctx.stroke(); 374 | ctx.restore(); 375 | // avatar 376 | ctx.save(); 377 | ctx.beginPath(); 378 | ctx.arc(padding.left + avatar.style.width / 2, padding.top + avatar.style.height / 2, avatar.style.width / 2, 0, Math.PI * 2); 379 | ctx.clip(); 380 | ctx.drawImage(avatar.image, padding.left, padding.top, avatar.style.width, avatar.style.height); 381 | ctx.restore(); 382 | } 383 | if (nickname) { 384 | // nickname 385 | ctx.save(); 386 | ctx.fillStyle = color; 387 | ctx.font = `${nickname.style.fontSize}px ${font.normal.fontFamily}`; 388 | ctx.fillText(nickname.text, nickname.style.x, nickname.style.y); 389 | ctx.restore(); 390 | } 391 | } 392 | 393 | export function drawFansNo(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, options: DrawOptions) { 394 | const { cardStyle, user } = options; 395 | const { color = "#ffffff", font } = cardStyle; 396 | const { no } = user; 397 | const { title, number } = no; 398 | // FANS NO. 399 | ctx.save(); 400 | ctx.fillStyle = color; 401 | ctx.font = `${title.style.fontSize}px ${font.mono.fontFamily}`; 402 | ctx.globalAlpha = font.mono.opacity; 403 | ctx.fillText(title.text, title.style.x, title.style.y); 404 | ctx.restore(); 405 | if (number) { 406 | // number 407 | ctx.save(); 408 | ctx.fillStyle = color; 409 | ctx.font = `${number.style.fontSize}px ${font.kenney.fontFamily}`; 410 | ctx.letterSpacing = `${number.style.letterSpacing}px`; 411 | ctx.fillText(number.text, number.style.x, number.style.y); 412 | ctx.restore(); 413 | } 414 | } 415 | 416 | export function drawDate(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, options: DrawOptions) { 417 | const { cardStyle, user } = options; 418 | const { color = "#ffffff", font } = cardStyle; 419 | const { date } = user; 420 | const { title, date: dateText } = date; 421 | // DATE 422 | ctx.save(); 423 | ctx.fillStyle = color; 424 | ctx.font = `${title.style.fontSize}px ${font.mono.fontFamily}`; 425 | ctx.globalAlpha = font.mono.opacity; 426 | ctx.fillText(title.text, title.style.x, title.style.y); 427 | ctx.restore(); 428 | if (dateText) { 429 | // date 430 | ctx.save(); 431 | ctx.fillStyle = color; 432 | ctx.font = `${dateText.style.fontSize}px ${font.normal.fontFamily}`; 433 | ctx.letterSpacing = `${dateText.style.letterSpacing}px`; 434 | ctx.fillText(dateText.text, dateText.style.x, dateText.style.y); 435 | ctx.restore(); 436 | } 437 | } 438 | 439 | export function compressImage(file: Blob, options?: { limit?: number; log?: boolean }) { 440 | const { limit, log = true } = options || {}; 441 | return new Promise((resolve, reject) => { 442 | // eslint-disable-next-line unused-imports/no-unused-vars 443 | const compressor = new Compressor(file, { 444 | convertTypes: [file.type], 445 | convertSize: isDefined(limit) ? limit : Infinity, 446 | success(result) { 447 | if (log) { 448 | // eslint-disable-next-line no-console 449 | console.log("compressed"); 450 | } 451 | resolve(result); 452 | }, 453 | error(err) { 454 | reject(err); 455 | }, 456 | }); 457 | }); 458 | } 459 | 460 | export function fitBackground(imageWidth: number, imageHeight: number) { 461 | const aspectRatio = 1 / 0.4115; 462 | 463 | let width, height, x, y; 464 | 465 | if (imageWidth / imageHeight > aspectRatio) { 466 | height = imageHeight; 467 | width = height * aspectRatio; 468 | x = (imageWidth - width) / 2; 469 | y = 0; 470 | } else { 471 | width = imageWidth; 472 | height = width / aspectRatio; 473 | x = 0; 474 | y = (imageHeight - height) / 2; 475 | } 476 | 477 | return { 478 | origin: [x, y] as [number, number], 479 | size: [width, height] as [number, number], 480 | }; 481 | } 482 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 11 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 12 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 13 | const computed: typeof import('vue')['computed'] 14 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 15 | const computedEager: typeof import('@vueuse/core')['computedEager'] 16 | const computedInject: typeof import('@vueuse/core')['computedInject'] 17 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 18 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 19 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 20 | const createApp: typeof import('vue')['createApp'] 21 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 22 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 23 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 24 | const createPinia: typeof import('pinia')['createPinia'] 25 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 26 | const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] 27 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 28 | const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] 29 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 30 | const customRef: typeof import('vue')['customRef'] 31 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 32 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 33 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 34 | const defineComponent: typeof import('vue')['defineComponent'] 35 | const defineStore: typeof import('pinia')['defineStore'] 36 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 37 | const effectScope: typeof import('vue')['effectScope'] 38 | const extendRef: typeof import('@vueuse/core')['extendRef'] 39 | const getActivePinia: typeof import('pinia')['getActivePinia'] 40 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 41 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 42 | const h: typeof import('vue')['h'] 43 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 44 | const inferFontColor: typeof import('./src/composables/usePalette')['inferFontColor'] 45 | const inject: typeof import('vue')['inject'] 46 | const injectLocal: typeof import('@vueuse/core')['injectLocal'] 47 | const isDark: typeof import('./src/composables/isDark')['isDark'] 48 | const isDefined: typeof import('@vueuse/core')['isDefined'] 49 | const isProxy: typeof import('vue')['isProxy'] 50 | const isReactive: typeof import('vue')['isReactive'] 51 | const isReadonly: typeof import('vue')['isReadonly'] 52 | const isRef: typeof import('vue')['isRef'] 53 | const log: typeof import('@bernankez/utils')['log'] 54 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 55 | const mapActions: typeof import('pinia')['mapActions'] 56 | const mapGetters: typeof import('pinia')['mapGetters'] 57 | const mapState: typeof import('pinia')['mapState'] 58 | const mapStores: typeof import('pinia')['mapStores'] 59 | const mapWritableState: typeof import('pinia')['mapWritableState'] 60 | const markRaw: typeof import('vue')['markRaw'] 61 | const nextTick: typeof import('vue')['nextTick'] 62 | const onActivated: typeof import('vue')['onActivated'] 63 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 64 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 65 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 66 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 67 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 68 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 69 | const onDeactivated: typeof import('vue')['onDeactivated'] 70 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 71 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 72 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 73 | const onMounted: typeof import('vue')['onMounted'] 74 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 75 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 76 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 77 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 78 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 79 | const onUnmounted: typeof import('vue')['onUnmounted'] 80 | const onUpdated: typeof import('vue')['onUpdated'] 81 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 82 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 83 | const provide: typeof import('vue')['provide'] 84 | const provideLocal: typeof import('@vueuse/core')['provideLocal'] 85 | const reactify: typeof import('@vueuse/core')['reactify'] 86 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 87 | const reactive: typeof import('vue')['reactive'] 88 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 89 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 90 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 91 | const readonly: typeof import('vue')['readonly'] 92 | const ref: typeof import('vue')['ref'] 93 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 94 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 95 | const refDefault: typeof import('@vueuse/core')['refDefault'] 96 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 97 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 98 | const resolveComponent: typeof import('vue')['resolveComponent'] 99 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 100 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 101 | const setActivePinia: typeof import('pinia')['setActivePinia'] 102 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 103 | const shallowReactive: typeof import('vue')['shallowReactive'] 104 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 105 | const shallowRef: typeof import('vue')['shallowRef'] 106 | const storeToRefs: typeof import('pinia')['storeToRefs'] 107 | const syncRef: typeof import('@vueuse/core')['syncRef'] 108 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 109 | const templateRef: typeof import('@vueuse/core')['templateRef'] 110 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 111 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 112 | const toRaw: typeof import('vue')['toRaw'] 113 | const toReactive: typeof import('@vueuse/core')['toReactive'] 114 | const toRef: typeof import('vue')['toRef'] 115 | const toRefs: typeof import('vue')['toRefs'] 116 | const toValue: typeof import('vue')['toValue'] 117 | const triggerRef: typeof import('vue')['triggerRef'] 118 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 119 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 120 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 121 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 122 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 123 | const unref: typeof import('vue')['unref'] 124 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 125 | const until: typeof import('@vueuse/core')['until'] 126 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 127 | const useAnimate: typeof import('@vueuse/core')['useAnimate'] 128 | const useAppStore: typeof import('./src/store/app')['useAppStore'] 129 | const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] 130 | const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] 131 | const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] 132 | const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] 133 | const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] 134 | const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] 135 | const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] 136 | const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] 137 | const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] 138 | const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] 139 | const useArraySome: typeof import('@vueuse/core')['useArraySome'] 140 | const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] 141 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 142 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 143 | const useAttrs: typeof import('vue')['useAttrs'] 144 | const useBase64: typeof import('@vueuse/core')['useBase64'] 145 | const useBattery: typeof import('@vueuse/core')['useBattery'] 146 | const useBlobUrl: typeof import('./src/composables/useBlobUrl')['useBlobUrl'] 147 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 148 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 149 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 150 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 151 | const useCached: typeof import('@vueuse/core')['useCached'] 152 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 153 | const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] 154 | const useCloned: typeof import('@vueuse/core')['useCloned'] 155 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 156 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 157 | const useCounter: typeof import('@vueuse/core')['useCounter'] 158 | const useCssModule: typeof import('vue')['useCssModule'] 159 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 160 | const useCssVars: typeof import('vue')['useCssVars'] 161 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 162 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 163 | const useDark: typeof import('@vueuse/core')['useDark'] 164 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 165 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 166 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 167 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 168 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 169 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 170 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 171 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 172 | const useDialog: typeof import('naive-ui')['useDialog'] 173 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 174 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 175 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 176 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 177 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 178 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 179 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 180 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 181 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 182 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 183 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 184 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 185 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 186 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 187 | const useFetch: typeof import('@vueuse/core')['useFetch'] 188 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 189 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 190 | const useFocus: typeof import('@vueuse/core')['useFocus'] 191 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 192 | const useFont: typeof import('./src/composables/useFont')['useFont'] 193 | const useFps: typeof import('@vueuse/core')['useFps'] 194 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 195 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 196 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 197 | const useI18n: typeof import('vue-i18n')['useI18n'] 198 | const useId: typeof import('vue')['useId'] 199 | const useIdle: typeof import('@vueuse/core')['useIdle'] 200 | const useImage: typeof import('@vueuse/core')['useImage'] 201 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 202 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 203 | const useInterval: typeof import('@vueuse/core')['useInterval'] 204 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 205 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 206 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 207 | const useLink: typeof import('vue-router')['useLink'] 208 | const useLoadingBar: typeof import('naive-ui')['useLoadingBar'] 209 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 210 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 211 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 212 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 213 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 214 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 215 | const useMemory: typeof import('@vueuse/core')['useMemory'] 216 | const useMergedState: typeof import('@bernankez/utils/vue')['useMergedState'] 217 | const useMessage: typeof import('naive-ui')['useMessage'] 218 | const useModel: typeof import('vue')['useModel'] 219 | const useMounted: typeof import('@vueuse/core')['useMounted'] 220 | const useMouse: typeof import('@vueuse/core')['useMouse'] 221 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 222 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 223 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 224 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 225 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 226 | const useNotification: typeof import('naive-ui')['useNotification'] 227 | const useNow: typeof import('@vueuse/core')['useNow'] 228 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 229 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 230 | const useOnline: typeof import('@vueuse/core')['useOnline'] 231 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 232 | const usePalette: typeof import('./src/composables/usePalette')['usePalette'] 233 | const useParallax: typeof import('@vueuse/core')['useParallax'] 234 | const useParentElement: typeof import('@vueuse/core')['useParentElement'] 235 | const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] 236 | const usePermission: typeof import('@vueuse/core')['usePermission'] 237 | const usePointer: typeof import('@vueuse/core')['usePointer'] 238 | const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] 239 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 240 | const usePost: typeof import('./src/composables/usePost')['usePost'] 241 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 242 | const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] 243 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 244 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 245 | const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] 246 | const usePrevious: typeof import('@vueuse/core')['usePrevious'] 247 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 248 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 249 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 250 | const useRoute: typeof import('vue-router')['useRoute'] 251 | const useRouter: typeof import('vue-router')['useRouter'] 252 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 253 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 254 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 255 | const useScroll: typeof import('@vueuse/core')['useScroll'] 256 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 257 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 258 | const useShare: typeof import('@vueuse/core')['useShare'] 259 | const useSidebarStore: typeof import('./src/store/sidebar')['useSidebarStore'] 260 | const useSlots: typeof import('vue')['useSlots'] 261 | const useSorted: typeof import('@vueuse/core')['useSorted'] 262 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 263 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 264 | const useStepper: typeof import('@vueuse/core')['useStepper'] 265 | const useStorage: typeof import('@vueuse/core')['useStorage'] 266 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 267 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 268 | const useSupported: typeof import('@vueuse/core')['useSupported'] 269 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 270 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 271 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 272 | const useTemplateStore: typeof import('./src/store/template')['useTemplateStore'] 273 | const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] 274 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 275 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 276 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 277 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 278 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 279 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 280 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 281 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 282 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 283 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 284 | const useTitle: typeof import('@vueuse/core')['useTitle'] 285 | const useToNumber: typeof import('@vueuse/core')['useToNumber'] 286 | const useToString: typeof import('@vueuse/core')['useToString'] 287 | const useToggle: typeof import('@vueuse/core')['useToggle'] 288 | const useTransition: typeof import('@vueuse/core')['useTransition'] 289 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 290 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 291 | const useUserStore: typeof import('./src/store/user')['useUserStore'] 292 | const useVModel: typeof import('@vueuse/core')['useVModel'] 293 | const useVModels: typeof import('@vueuse/core')['useVModels'] 294 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 295 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 296 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 297 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 298 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 299 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 300 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 301 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 302 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 303 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 304 | const watch: typeof import('vue')['watch'] 305 | const watchArray: typeof import('@vueuse/core')['watchArray'] 306 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 307 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 308 | const watchDeep: typeof import('@vueuse/core')['watchDeep'] 309 | const watchEffect: typeof import('vue')['watchEffect'] 310 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 311 | const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] 312 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 313 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 314 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 315 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 316 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 317 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 318 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 319 | const whenever: typeof import('@vueuse/core')['whenever'] 320 | } 321 | // for type re-export 322 | declare global { 323 | // @ts-ignore 324 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 325 | import('vue') 326 | } 327 | // for vue template auto import 328 | import { UnwrapRef } from 'vue' 329 | declare module 'vue' { 330 | interface GlobalComponents {} 331 | interface ComponentCustomProperties { 332 | readonly EffectScope: UnwrapRef 333 | readonly acceptHMRUpdate: UnwrapRef 334 | readonly asyncComputed: UnwrapRef 335 | readonly autoResetRef: UnwrapRef 336 | readonly computed: UnwrapRef 337 | readonly computedAsync: UnwrapRef 338 | readonly computedEager: UnwrapRef 339 | readonly computedInject: UnwrapRef 340 | readonly computedWithControl: UnwrapRef 341 | readonly controlledComputed: UnwrapRef 342 | readonly controlledRef: UnwrapRef 343 | readonly createApp: UnwrapRef 344 | readonly createEventHook: UnwrapRef 345 | readonly createGlobalState: UnwrapRef 346 | readonly createInjectionState: UnwrapRef 347 | readonly createPinia: UnwrapRef 348 | readonly createReactiveFn: UnwrapRef 349 | readonly createReusableTemplate: UnwrapRef 350 | readonly createSharedComposable: UnwrapRef 351 | readonly createTemplatePromise: UnwrapRef 352 | readonly createUnrefFn: UnwrapRef 353 | readonly customRef: UnwrapRef 354 | readonly debouncedRef: UnwrapRef 355 | readonly debouncedWatch: UnwrapRef 356 | readonly defineAsyncComponent: UnwrapRef 357 | readonly defineComponent: UnwrapRef 358 | readonly defineStore: UnwrapRef 359 | readonly eagerComputed: UnwrapRef 360 | readonly effectScope: UnwrapRef 361 | readonly extendRef: UnwrapRef 362 | readonly getActivePinia: UnwrapRef 363 | readonly getCurrentInstance: UnwrapRef 364 | readonly getCurrentScope: UnwrapRef 365 | readonly h: UnwrapRef 366 | readonly ignorableWatch: UnwrapRef 367 | readonly inferFontColor: UnwrapRef 368 | readonly inject: UnwrapRef 369 | readonly injectLocal: UnwrapRef 370 | readonly isDark: UnwrapRef 371 | readonly isDefined: UnwrapRef 372 | readonly isProxy: UnwrapRef 373 | readonly isReactive: UnwrapRef 374 | readonly isReadonly: UnwrapRef 375 | readonly isRef: UnwrapRef 376 | readonly log: UnwrapRef 377 | readonly makeDestructurable: UnwrapRef 378 | readonly mapActions: UnwrapRef 379 | readonly mapGetters: UnwrapRef 380 | readonly mapState: UnwrapRef 381 | readonly mapStores: UnwrapRef 382 | readonly mapWritableState: UnwrapRef 383 | readonly markRaw: UnwrapRef 384 | readonly nextTick: UnwrapRef 385 | readonly onActivated: UnwrapRef 386 | readonly onBeforeMount: UnwrapRef 387 | readonly onBeforeRouteLeave: UnwrapRef 388 | readonly onBeforeRouteUpdate: UnwrapRef 389 | readonly onBeforeUnmount: UnwrapRef 390 | readonly onBeforeUpdate: UnwrapRef 391 | readonly onClickOutside: UnwrapRef 392 | readonly onDeactivated: UnwrapRef 393 | readonly onErrorCaptured: UnwrapRef 394 | readonly onKeyStroke: UnwrapRef 395 | readonly onLongPress: UnwrapRef 396 | readonly onMounted: UnwrapRef 397 | readonly onRenderTracked: UnwrapRef 398 | readonly onRenderTriggered: UnwrapRef 399 | readonly onScopeDispose: UnwrapRef 400 | readonly onServerPrefetch: UnwrapRef 401 | readonly onStartTyping: UnwrapRef 402 | readonly onUnmounted: UnwrapRef 403 | readonly onUpdated: UnwrapRef 404 | readonly onWatcherCleanup: UnwrapRef 405 | readonly pausableWatch: UnwrapRef 406 | readonly provide: UnwrapRef 407 | readonly provideLocal: UnwrapRef 408 | readonly reactify: UnwrapRef 409 | readonly reactifyObject: UnwrapRef 410 | readonly reactive: UnwrapRef 411 | readonly reactiveComputed: UnwrapRef 412 | readonly reactiveOmit: UnwrapRef 413 | readonly reactivePick: UnwrapRef 414 | readonly readonly: UnwrapRef 415 | readonly ref: UnwrapRef 416 | readonly refAutoReset: UnwrapRef 417 | readonly refDebounced: UnwrapRef 418 | readonly refDefault: UnwrapRef 419 | readonly refThrottled: UnwrapRef 420 | readonly refWithControl: UnwrapRef 421 | readonly resolveComponent: UnwrapRef 422 | readonly resolveRef: UnwrapRef 423 | readonly resolveUnref: UnwrapRef 424 | readonly setActivePinia: UnwrapRef 425 | readonly setMapStoreSuffix: UnwrapRef 426 | readonly shallowReactive: UnwrapRef 427 | readonly shallowReadonly: UnwrapRef 428 | readonly shallowRef: UnwrapRef 429 | readonly storeToRefs: UnwrapRef 430 | readonly syncRef: UnwrapRef 431 | readonly syncRefs: UnwrapRef 432 | readonly templateRef: UnwrapRef 433 | readonly throttledRef: UnwrapRef 434 | readonly throttledWatch: UnwrapRef 435 | readonly toRaw: UnwrapRef 436 | readonly toReactive: UnwrapRef 437 | readonly toRef: UnwrapRef 438 | readonly toRefs: UnwrapRef 439 | readonly toValue: UnwrapRef 440 | readonly triggerRef: UnwrapRef 441 | readonly tryOnBeforeMount: UnwrapRef 442 | readonly tryOnBeforeUnmount: UnwrapRef 443 | readonly tryOnMounted: UnwrapRef 444 | readonly tryOnScopeDispose: UnwrapRef 445 | readonly tryOnUnmounted: UnwrapRef 446 | readonly unref: UnwrapRef 447 | readonly unrefElement: UnwrapRef 448 | readonly until: UnwrapRef 449 | readonly useActiveElement: UnwrapRef 450 | readonly useAnimate: UnwrapRef 451 | readonly useAppStore: UnwrapRef 452 | readonly useArrayDifference: UnwrapRef 453 | readonly useArrayEvery: UnwrapRef 454 | readonly useArrayFilter: UnwrapRef 455 | readonly useArrayFind: UnwrapRef 456 | readonly useArrayFindIndex: UnwrapRef 457 | readonly useArrayFindLast: UnwrapRef 458 | readonly useArrayIncludes: UnwrapRef 459 | readonly useArrayJoin: UnwrapRef 460 | readonly useArrayMap: UnwrapRef 461 | readonly useArrayReduce: UnwrapRef 462 | readonly useArraySome: UnwrapRef 463 | readonly useArrayUnique: UnwrapRef 464 | readonly useAsyncQueue: UnwrapRef 465 | readonly useAsyncState: UnwrapRef 466 | readonly useAttrs: UnwrapRef 467 | readonly useBase64: UnwrapRef 468 | readonly useBattery: UnwrapRef 469 | readonly useBlobUrl: UnwrapRef 470 | readonly useBluetooth: UnwrapRef 471 | readonly useBreakpoints: UnwrapRef 472 | readonly useBroadcastChannel: UnwrapRef 473 | readonly useBrowserLocation: UnwrapRef 474 | readonly useCached: UnwrapRef 475 | readonly useClipboard: UnwrapRef 476 | readonly useClipboardItems: UnwrapRef 477 | readonly useCloned: UnwrapRef 478 | readonly useColorMode: UnwrapRef 479 | readonly useConfirmDialog: UnwrapRef 480 | readonly useCounter: UnwrapRef 481 | readonly useCssModule: UnwrapRef 482 | readonly useCssVar: UnwrapRef 483 | readonly useCssVars: UnwrapRef 484 | readonly useCurrentElement: UnwrapRef 485 | readonly useCycleList: UnwrapRef 486 | readonly useDark: UnwrapRef 487 | readonly useDateFormat: UnwrapRef 488 | readonly useDebounce: UnwrapRef 489 | readonly useDebounceFn: UnwrapRef 490 | readonly useDebouncedRefHistory: UnwrapRef 491 | readonly useDeviceMotion: UnwrapRef 492 | readonly useDeviceOrientation: UnwrapRef 493 | readonly useDevicePixelRatio: UnwrapRef 494 | readonly useDevicesList: UnwrapRef 495 | readonly useDialog: UnwrapRef 496 | readonly useDisplayMedia: UnwrapRef 497 | readonly useDocumentVisibility: UnwrapRef 498 | readonly useDraggable: UnwrapRef 499 | readonly useDropZone: UnwrapRef 500 | readonly useElementBounding: UnwrapRef 501 | readonly useElementByPoint: UnwrapRef 502 | readonly useElementHover: UnwrapRef 503 | readonly useElementSize: UnwrapRef 504 | readonly useElementVisibility: UnwrapRef 505 | readonly useEventBus: UnwrapRef 506 | readonly useEventListener: UnwrapRef 507 | readonly useEventSource: UnwrapRef 508 | readonly useEyeDropper: UnwrapRef 509 | readonly useFavicon: UnwrapRef 510 | readonly useFetch: UnwrapRef 511 | readonly useFileDialog: UnwrapRef 512 | readonly useFileSystemAccess: UnwrapRef 513 | readonly useFocus: UnwrapRef 514 | readonly useFocusWithin: UnwrapRef 515 | readonly useFont: UnwrapRef 516 | readonly useFps: UnwrapRef 517 | readonly useFullscreen: UnwrapRef 518 | readonly useGamepad: UnwrapRef 519 | readonly useGeolocation: UnwrapRef 520 | readonly useI18n: UnwrapRef 521 | readonly useId: UnwrapRef 522 | readonly useIdle: UnwrapRef 523 | readonly useImage: UnwrapRef 524 | readonly useInfiniteScroll: UnwrapRef 525 | readonly useIntersectionObserver: UnwrapRef 526 | readonly useInterval: UnwrapRef 527 | readonly useIntervalFn: UnwrapRef 528 | readonly useKeyModifier: UnwrapRef 529 | readonly useLastChanged: UnwrapRef 530 | readonly useLink: UnwrapRef 531 | readonly useLoadingBar: UnwrapRef 532 | readonly useLocalStorage: UnwrapRef 533 | readonly useMagicKeys: UnwrapRef 534 | readonly useManualRefHistory: UnwrapRef 535 | readonly useMediaControls: UnwrapRef 536 | readonly useMediaQuery: UnwrapRef 537 | readonly useMemoize: UnwrapRef 538 | readonly useMemory: UnwrapRef 539 | readonly useMergedState: UnwrapRef 540 | readonly useMessage: UnwrapRef 541 | readonly useModel: UnwrapRef 542 | readonly useMounted: UnwrapRef 543 | readonly useMouse: UnwrapRef 544 | readonly useMouseInElement: UnwrapRef 545 | readonly useMousePressed: UnwrapRef 546 | readonly useMutationObserver: UnwrapRef 547 | readonly useNavigatorLanguage: UnwrapRef 548 | readonly useNetwork: UnwrapRef 549 | readonly useNotification: UnwrapRef 550 | readonly useNow: UnwrapRef 551 | readonly useObjectUrl: UnwrapRef 552 | readonly useOffsetPagination: UnwrapRef 553 | readonly useOnline: UnwrapRef 554 | readonly usePageLeave: UnwrapRef 555 | readonly usePalette: UnwrapRef 556 | readonly useParallax: UnwrapRef 557 | readonly useParentElement: UnwrapRef 558 | readonly usePerformanceObserver: UnwrapRef 559 | readonly usePermission: UnwrapRef 560 | readonly usePointer: UnwrapRef 561 | readonly usePointerLock: UnwrapRef 562 | readonly usePointerSwipe: UnwrapRef 563 | readonly usePost: UnwrapRef 564 | readonly usePreferredColorScheme: UnwrapRef 565 | readonly usePreferredContrast: UnwrapRef 566 | readonly usePreferredDark: UnwrapRef 567 | readonly usePreferredLanguages: UnwrapRef 568 | readonly usePreferredReducedMotion: UnwrapRef 569 | readonly usePrevious: UnwrapRef 570 | readonly useRafFn: UnwrapRef 571 | readonly useRefHistory: UnwrapRef 572 | readonly useResizeObserver: UnwrapRef 573 | readonly useRoute: UnwrapRef 574 | readonly useRouter: UnwrapRef 575 | readonly useScreenOrientation: UnwrapRef 576 | readonly useScreenSafeArea: UnwrapRef 577 | readonly useScriptTag: UnwrapRef 578 | readonly useScroll: UnwrapRef 579 | readonly useScrollLock: UnwrapRef 580 | readonly useSessionStorage: UnwrapRef 581 | readonly useShare: UnwrapRef 582 | readonly useSidebarStore: UnwrapRef 583 | readonly useSlots: UnwrapRef 584 | readonly useSorted: UnwrapRef 585 | readonly useSpeechRecognition: UnwrapRef 586 | readonly useSpeechSynthesis: UnwrapRef 587 | readonly useStepper: UnwrapRef 588 | readonly useStorage: UnwrapRef 589 | readonly useStorageAsync: UnwrapRef 590 | readonly useStyleTag: UnwrapRef 591 | readonly useSupported: UnwrapRef 592 | readonly useSwipe: UnwrapRef 593 | readonly useTemplateRef: UnwrapRef 594 | readonly useTemplateRefsList: UnwrapRef 595 | readonly useTemplateStore: UnwrapRef 596 | readonly useTextDirection: UnwrapRef 597 | readonly useTextSelection: UnwrapRef 598 | readonly useTextareaAutosize: UnwrapRef 599 | readonly useThrottle: UnwrapRef 600 | readonly useThrottleFn: UnwrapRef 601 | readonly useThrottledRefHistory: UnwrapRef 602 | readonly useTimeAgo: UnwrapRef 603 | readonly useTimeout: UnwrapRef 604 | readonly useTimeoutFn: UnwrapRef 605 | readonly useTimeoutPoll: UnwrapRef 606 | readonly useTimestamp: UnwrapRef 607 | readonly useTitle: UnwrapRef 608 | readonly useToNumber: UnwrapRef 609 | readonly useToString: UnwrapRef 610 | readonly useToggle: UnwrapRef 611 | readonly useTransition: UnwrapRef 612 | readonly useUrlSearchParams: UnwrapRef 613 | readonly useUserMedia: UnwrapRef 614 | readonly useUserStore: UnwrapRef 615 | readonly useVModel: UnwrapRef 616 | readonly useVModels: UnwrapRef 617 | readonly useVibrate: UnwrapRef 618 | readonly useVirtualList: UnwrapRef 619 | readonly useWakeLock: UnwrapRef 620 | readonly useWebNotification: UnwrapRef 621 | readonly useWebSocket: UnwrapRef 622 | readonly useWebWorker: UnwrapRef 623 | readonly useWebWorkerFn: UnwrapRef 624 | readonly useWindowFocus: UnwrapRef 625 | readonly useWindowScroll: UnwrapRef 626 | readonly useWindowSize: UnwrapRef 627 | readonly watch: UnwrapRef 628 | readonly watchArray: UnwrapRef 629 | readonly watchAtMost: UnwrapRef 630 | readonly watchDebounced: UnwrapRef 631 | readonly watchDeep: UnwrapRef 632 | readonly watchEffect: UnwrapRef 633 | readonly watchIgnorable: UnwrapRef 634 | readonly watchImmediate: UnwrapRef 635 | readonly watchOnce: UnwrapRef 636 | readonly watchPausable: UnwrapRef 637 | readonly watchPostEffect: UnwrapRef 638 | readonly watchSyncEffect: UnwrapRef 639 | readonly watchThrottled: UnwrapRef 640 | readonly watchTriggerable: UnwrapRef 641 | readonly watchWithFilter: UnwrapRef 642 | readonly whenever: UnwrapRef 643 | } 644 | } 645 | --------------------------------------------------------------------------------