├── .eslintrc.cjs ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── .vscode └── extensions.json ├── README.md ├── auto-imports.d.ts ├── env.d.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── icon.webp └── preview.webp ├── src ├── App.vue ├── assets │ ├── data │ │ └── log.ts │ ├── images │ │ ├── default.webp │ │ ├── 分组按钮.webp │ │ ├── 按钮.webp │ │ ├── 提示按钮.webp │ │ ├── 提示背景.webp │ │ ├── 毛笔按钮.webp │ │ ├── 游记.webp │ │ ├── 菜单按钮.webp │ │ ├── 设置.webp │ │ └── 设置按钮.webp │ ├── scripts │ │ ├── database.ts │ │ ├── event.ts │ │ ├── file.ts │ │ ├── hotkey.ts │ │ ├── popup.ts │ │ ├── portraits.ts │ │ └── update.ts │ └── styles │ │ └── function.styl ├── components │ ├── Common │ │ ├── Btn.vue │ │ ├── Icon.tsx │ │ ├── Keyboard.vue │ │ └── Window.vue │ ├── Index.vue │ ├── Index │ │ ├── Portraits.vue │ │ └── Setting.vue │ ├── Menu.vue │ ├── Popup │ │ ├── Confirm │ │ │ ├── Confirm.vue │ │ │ ├── data.ts │ │ │ └── index.ts │ │ ├── Cropper │ │ │ ├── Cropper.vue │ │ │ ├── data.ts │ │ │ └── index.ts │ │ ├── Data.vue │ │ ├── Input │ │ │ ├── Input.vue │ │ │ ├── data.ts │ │ │ └── index.ts │ │ ├── Loading.vue │ │ ├── Log.vue │ │ └── Select │ │ │ ├── Select.vue │ │ │ ├── data.ts │ │ │ └── index.ts │ ├── Shadow.vue │ └── Tip.vue ├── main.styl ├── main.ts ├── store │ ├── data.ts │ └── setting.ts └── types.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | }, 15 | rules: { 16 | 'vue/multi-word-component-names': 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://afdian.com/a/blacktune'] -------------------------------------------------------------------------------- /.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 | .DS_Store 12 | dev-dist 13 | dist 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=workbox-window 2 | public-hoist-pattern[]=vue-picture-cropper 3 | public-hoist-pattern[]=super-image-cropper -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none", 8 | "singleAttributePerLine": true, 9 | "endOfLine": "auto", 10 | "plugins": [ 11 | "prettier-plugin-stylus-format" 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

影神图

2 |

黑神话:悟空 - 影神图生成网站

3 | 4 | ![预览图](public/preview.webp) 5 | 6 | ## TODO 7 | - 替换更清晰的美术素材 8 | - 增加用户引导 9 | - 完善字体 10 | 11 | ## 相关项目 12 | - 崩坏:星穹铁道光锥生成器 13 | - https://github.com/blacktunes/sr-light-cone 14 | - https://light.shenmedouyou.top/ 15 | - 崩坏:星穹铁道短信生成器 16 | - https://github.com/blacktunes/sr-message-maker 17 | - https://sr.shenmedouyou.top/ 18 | - 崩坏:星穹铁道罗浮杂俎生成器 19 | - https://github.com/blacktunes/sr-ghostly-grove 20 | - https://ghostly.shenmedouyou.top/ 21 | - 绝区零绳网生成器 22 | - https://github.com/blacktunes/interknot 23 | - https://interknot.shenmedouyou.top/ 24 | - 碧蓝航线JUUs生成器 25 | - https://github.com/blacktunes/juus-maker 26 | - https://juus.shenmedouyou.top/ 27 | - 工具库 28 | - https://github.com/blacktunes/star-rail-vue 29 | 30 | ## 支持 31 | 如果你喜欢这个项目,可以给个⭐️或者[请我喝杯柠檬水](https://afdian.com/a/blacktune) 32 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const customRef: typeof import('vue')['customRef'] 12 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 13 | const defineComponent: typeof import('vue')['defineComponent'] 14 | const effectScope: typeof import('vue')['effectScope'] 15 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 16 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 17 | const h: typeof import('vue')['h'] 18 | const inject: typeof import('vue')['inject'] 19 | const isProxy: typeof import('vue')['isProxy'] 20 | const isReactive: typeof import('vue')['isReactive'] 21 | const isReadonly: typeof import('vue')['isReadonly'] 22 | const isRef: typeof import('vue')['isRef'] 23 | const markRaw: typeof import('vue')['markRaw'] 24 | const nextTick: typeof import('vue')['nextTick'] 25 | const onActivated: typeof import('vue')['onActivated'] 26 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 27 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 28 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 29 | const onDeactivated: typeof import('vue')['onDeactivated'] 30 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 31 | const onMounted: typeof import('vue')['onMounted'] 32 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 33 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 34 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 35 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 36 | const onUnmounted: typeof import('vue')['onUnmounted'] 37 | const onUpdated: typeof import('vue')['onUpdated'] 38 | const provide: typeof import('vue')['provide'] 39 | const reactive: typeof import('vue')['reactive'] 40 | const readonly: typeof import('vue')['readonly'] 41 | const ref: typeof import('vue')['ref'] 42 | const resolveComponent: typeof import('vue')['resolveComponent'] 43 | const shallowReactive: typeof import('vue')['shallowReactive'] 44 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 45 | const shallowRef: typeof import('vue')['shallowRef'] 46 | const toRaw: typeof import('vue')['toRaw'] 47 | const toRef: typeof import('vue')['toRef'] 48 | const toRefs: typeof import('vue')['toRefs'] 49 | const toValue: typeof import('vue')['toValue'] 50 | const triggerRef: typeof import('vue')['triggerRef'] 51 | const unref: typeof import('vue')['unref'] 52 | const useAttrs: typeof import('vue')['useAttrs'] 53 | const useCssModule: typeof import('vue')['useCssModule'] 54 | const useCssVars: typeof import('vue')['useCssVars'] 55 | const useSlots: typeof import('vue')['useSlots'] 56 | const watch: typeof import('vue')['watch'] 57 | const watchEffect: typeof import('vue')['watchEffect'] 58 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 59 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 60 | } 61 | // for type re-export 62 | declare global { 63 | // @ts-ignore 64 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 65 | import('vue') 66 | } 67 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | interface Window { 5 | BUILD_TIME: Date 6 | } 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 影神图 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portraits", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite --port 9002 --host", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "format": "prettier --write src/" 14 | }, 15 | "dependencies": { 16 | "@ckpack/parameter": "^2.9.0", 17 | "mitt": "^3.0.1", 18 | "star-rail-vue": "^1.3.9", 19 | "vue": "^3.5.4", 20 | "vue-dompurify-html": "^5.1.0" 21 | }, 22 | "devDependencies": { 23 | "@rushstack/eslint-patch": "^1.10.4", 24 | "@tsconfig/node18": "^18.2.4", 25 | "@types/node": "^18.19.50", 26 | "@vitejs/plugin-vue": "^5.1.3", 27 | "@vitejs/plugin-vue-jsx": "^3.1.0", 28 | "@vue/eslint-config-prettier": "^9.0.0", 29 | "@vue/eslint-config-typescript": "^13.0.0", 30 | "@vue/tsconfig": "^0.5.1", 31 | "eslint": "^8.57.0", 32 | "eslint-plugin-vue": "^9.28.0", 33 | "npm-run-all2": "^6.2.2", 34 | "prettier": "^3.3.3", 35 | "prettier-plugin-stylus-format": "^1.0.1", 36 | "stylus": "^0.63.0", 37 | "typescript": "^5.6.2", 38 | "unplugin-auto-import": "^0.17.8", 39 | "vite": "^5.4.4", 40 | "vite-plugin-pwa": "^0.19.8", 41 | "vite-plugin-vue-devtools": "^7.4.4", 42 | "vue-tsc": "^2.1.6" 43 | } 44 | } -------------------------------------------------------------------------------- /public/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/public/icon.webp -------------------------------------------------------------------------------- /public/preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/public/preview.webp -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/assets/data/log.ts: -------------------------------------------------------------------------------- 1 | import type { LogData } from 'star-rail-vue' 2 | 3 | const log: LogData[] = [ 4 | { 5 | time: '2024-10-01', 6 | text: [ 7 | { 8 | text: '优化界面和操作', 9 | info: [ 10 | '默认显示游戏里的四个分类', 11 | '增加新建影神图时分类选择页', 12 | '增加删除快捷键(Del)', 13 | '优化按钮动画效果' 14 | ] 15 | }, 16 | 17 | { 18 | text: '增加影神图的管理菜单', 19 | info: '选择影神图时右键(移动端长按)可打开' 20 | } 21 | ] 22 | }, 23 | { 24 | time: '2024-09-29', 25 | text: [ 26 | { 27 | text: '测试版上线', 28 | highlight: true 29 | } 30 | ] 31 | } 32 | ] 33 | 34 | export default log 35 | -------------------------------------------------------------------------------- /src/assets/images/default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/default.webp -------------------------------------------------------------------------------- /src/assets/images/分组按钮.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/分组按钮.webp -------------------------------------------------------------------------------- /src/assets/images/按钮.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/按钮.webp -------------------------------------------------------------------------------- /src/assets/images/提示按钮.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/提示按钮.webp -------------------------------------------------------------------------------- /src/assets/images/提示背景.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/提示背景.webp -------------------------------------------------------------------------------- /src/assets/images/毛笔按钮.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/毛笔按钮.webp -------------------------------------------------------------------------------- /src/assets/images/游记.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/游记.webp -------------------------------------------------------------------------------- /src/assets/images/菜单按钮.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/菜单按钮.webp -------------------------------------------------------------------------------- /src/assets/images/设置.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/设置.webp -------------------------------------------------------------------------------- /src/assets/images/设置按钮.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacktunes/black-myth-wukong-portraits/76930d63a147e21f117b99c93cabd3a0280082f8/src/assets/images/设置按钮.webp -------------------------------------------------------------------------------- /src/assets/scripts/database.ts: -------------------------------------------------------------------------------- 1 | import defaultImage from '@/assets/images/default.webp' 2 | import { data } from '@/store/data' 3 | import { KEY } from '@/store/setting' 4 | import { createDatabase } from 'star-rail-vue' 5 | import { popupManager } from './popup' 6 | 7 | export const loadDatabase = () => { 8 | return new Promise((resolve) => { 9 | // 数据库加载超时 10 | const timeout = setTimeout(() => { 11 | popupManager.open('confirm', { 12 | text: [ 13 | '加载时间过长,可能是数据损坏', 14 | '点击确认可以继续使用,但是可能出现功能异常' 15 | ], 16 | fn: () => { 17 | popupManager.close('loading') 18 | }, 19 | close: () => { 20 | resolve() 21 | } 22 | }) 23 | }, 30 * 1000) 24 | 25 | createDatabase(KEY.DATABASE_NAME, '影神图') 26 | .add({ 27 | data: data, 28 | key: 'list', 29 | name: 'portraits', 30 | initial: [ 31 | { 32 | id: Date.now(), 33 | name: '咸鱼', 34 | type: '小妖', 35 | info: '咸鱼咸,咸鱼咸,咸鱼咸鱼咸。\n咕咕咕,咕咕咕,咕咕咕咕咕。', 36 | text: '所以到底是咸鱼还是鸽子?\n不知道,鸽了。', 37 | overlay: true, 38 | time: Date.now(), 39 | image: defaultImage 40 | } 41 | ] 42 | }) 43 | .next() 44 | .then(() => { 45 | popupManager.close('confirm') 46 | resolve() 47 | }) 48 | .catch((err) => { 49 | console.error(err) 50 | 51 | popupManager.open('confirm', { 52 | text: ['数据库初始化失败', '编辑的数据可能会丢失且不会被保存'], 53 | close: () => { 54 | resolve() 55 | } 56 | }) 57 | }) 58 | .finally(() => { 59 | clearTimeout(timeout) 60 | popupManager.close('loading') 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/scripts/event.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | type Events = { 4 | screenshot: void 5 | } 6 | 7 | export const emitter = mitt() 8 | -------------------------------------------------------------------------------- /src/assets/scripts/file.ts: -------------------------------------------------------------------------------- 1 | import { current, data } from '@/store/data' 2 | import { KEY, setting, state } from '@/store/setting' 3 | import { Parameter, setLocale, zhLocale } from '@ckpack/parameter' 4 | import { 5 | createDownloadFile, 6 | decompressFromArrayBuffer, 7 | decompressFromZip, 8 | screenshot 9 | } from 'star-rail-vue' 10 | import { popupManager } from './popup' 11 | 12 | const dataRule = { 13 | id: { 14 | isRequired: false, 15 | type: 'number' 16 | }, 17 | name: 'string', 18 | type: 'string', 19 | info: 'string', 20 | text: 'string', 21 | image: { 22 | isRequired: false, 23 | type: 'string' 24 | }, 25 | overlay: { 26 | isRequired: false, 27 | type: 'boolean' 28 | }, 29 | time: { 30 | isRequired: false, 31 | type: 'number' 32 | } 33 | } 34 | 35 | setLocale(zhLocale) 36 | const parameter = new Parameter() 37 | 38 | export const inputFile = async () => { 39 | const el = document.createElement('input') 40 | el.type = 'file' 41 | el.accept = `.png,${KEY.FILE_ACCEPT}` 42 | el.onchange = async () => { 43 | const file = el.files?.[0] 44 | if (file) { 45 | await importFile(file) 46 | } 47 | } 48 | el.click() 49 | el.remove() 50 | } 51 | 52 | export const importFile = async (file: File, open?: boolean) => { 53 | const accept = file.name.split('.').pop() 54 | if (`.${accept}` === KEY.FILE_ACCEPT) { 55 | const reader = new FileReader() 56 | reader.readAsArrayBuffer(file) 57 | reader.onload = (e) => { 58 | if (e.target?.result) { 59 | try { 60 | const newDataList: Portrait[] = decompressFromArrayBuffer(e.target.result as ArrayBuffer) 61 | let time = Date.now() 62 | let num = 0 63 | for (const i in newDataList) { 64 | newDataList[i].time = time 65 | newDataList[i].id = time++ 66 | const val = parameter.validate(dataRule, newDataList[i]) 67 | if (val) { 68 | val.forEach((err) => { 69 | console.warn(err.message) 70 | }) 71 | } else { 72 | data.list.push(newDataList[i]) 73 | num += 1 74 | } 75 | } 76 | if (num === 0) { 77 | popupManager.open('confirm', { 78 | text: ['影神图导入失败', '请检查文件是否正确'] 79 | }) 80 | } else { 81 | popupManager.open('confirm', { 82 | text: [`已添加${num}张新影神图`] 83 | }) 84 | } 85 | } catch (err) { 86 | popupManager.open('confirm', { 87 | text: ['影神图导入失败', String(err)] 88 | }) 89 | } 90 | } 91 | } 92 | } else { 93 | try { 94 | const newData = await decompressFromZip(file, KEY.RAW_NAME) 95 | const time = Date.now() 96 | newData.time = time 97 | newData.id = time 98 | const val = parameter.validate(dataRule, newData) 99 | if (val) { 100 | val.forEach((err) => { 101 | console.warn(err.message) 102 | }) 103 | throw Error() 104 | } else { 105 | data.list.push(newData) 106 | if (open) { 107 | state.window = 'index' 108 | state.group = newData.type 109 | state.ID = newData.id 110 | } else { 111 | popupManager.open('confirm', { 112 | text: ['已添加影神图', newData.name] 113 | }) 114 | } 115 | } 116 | } catch { 117 | popupManager.open('confirm', { 118 | text: ['该文件可能不是由影神图生成器生成或内容已被修改'] 119 | }) 120 | } 121 | } 122 | } 123 | 124 | export const exportFile = () => { 125 | const blob = createDownloadFile(data.list) 126 | const url = URL.createObjectURL(blob) 127 | const a = document.createElement('a') 128 | a.href = url 129 | a.download = `影神图 - ${new Date().toLocaleString()}${KEY.FILE_ACCEPT}` 130 | a.click() 131 | a.remove() 132 | URL.revokeObjectURL(url) 133 | } 134 | 135 | export const createScreenshot = (dom?: HTMLElement | null) => { 136 | if (popupManager.isLoading()) return 137 | 138 | state.screenshot = true 139 | popupManager.open('loading') 140 | nextTick(() => { 141 | if (dom) { 142 | if (!current.value) { 143 | popupManager.close('loading') 144 | return 145 | } 146 | 147 | screenshot( 148 | dom, 149 | { 150 | name: current.value.name, 151 | download: setting.download, 152 | data: { 153 | raw: JSON.stringify(toRaw(current.value)), 154 | filename: KEY.RAW_NAME 155 | } 156 | }, 157 | { pixelRatio: setting.quality } 158 | ) 159 | .catch(() => { 160 | popupManager.open('confirm', { 161 | text: ['影神图保存失败'] 162 | }) 163 | }) 164 | .finally(() => { 165 | setTimeout(() => { 166 | state.screenshot = false 167 | popupManager.close('loading') 168 | }, 1000) 169 | }) 170 | } else { 171 | state.screenshot = false 172 | popupManager.close('loading') 173 | } 174 | }) 175 | } 176 | -------------------------------------------------------------------------------- /src/assets/scripts/hotkey.ts: -------------------------------------------------------------------------------- 1 | import { current } from '@/store/data' 2 | import { setting, state } from '@/store/setting' 3 | import { popupManager } from './popup' 4 | import { deleteItem, startScreenshot } from './portraits' 5 | 6 | let flag = false 7 | 8 | export const windowChange = () => { 9 | if (state.window === 'index') { 10 | state.window = 'setting' 11 | } else { 12 | state.expand = false 13 | state.window = 'index' 14 | } 15 | } 16 | 17 | export const hotkey = () => { 18 | if (flag) return 19 | flag = true 20 | 21 | document.addEventListener('contextmenu', (e) => { 22 | e.preventDefault() 23 | }) 24 | 25 | document.addEventListener('click', (e) => { 26 | if (popupManager.isLoading()) return 27 | if ((e.target as HTMLElement).tagName.toLowerCase() === 'a') return 28 | if (popupManager.hasPopup()) { 29 | popupManager.closeCurrentComponent() 30 | } else if (setting.tip) { 31 | setting.tip = false 32 | } 33 | }) 34 | 35 | document.addEventListener('keydown', async (e) => { 36 | if (popupManager.isLoading() || setting.tip) return 37 | switch (e.key) { 38 | case 'Tab': 39 | e.preventDefault() 40 | return 41 | case 'a': 42 | if (popupManager.hasPopup()) return 43 | windowChange() 44 | return 45 | case 'd': 46 | if (popupManager.hasPopup()) return 47 | windowChange() 48 | return 49 | // 保存截图 50 | case 's': 51 | if (popupManager.hasPopup()) return 52 | if (!e.ctrlKey) return 53 | e.preventDefault() 54 | startScreenshot() 55 | return 56 | case 't': 57 | if (popupManager.hasPopup()) return 58 | if (state.window === 'index') state.expand = !state.expand 59 | return 60 | case 'Delete': 61 | if (popupManager.hasPopup() || state.window === 'setting' || !current.value) return 62 | deleteItem() 63 | return 64 | case 'Enter': 65 | popupManager.currentComponentConfirm() 66 | return 67 | case 'Escape': 68 | popupManager.closeCurrentComponent() 69 | return 70 | } 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/assets/scripts/popup.ts: -------------------------------------------------------------------------------- 1 | import { confirm } from '@/components/Popup/Confirm' 2 | import { cropper } from '@/components/Popup/Cropper' 3 | import Data from '@/components/Popup/Data.vue' 4 | import { input } from '@/components/Popup/Input' 5 | import Loading from '@/components/Popup/Loading.vue' 6 | import Log from '@/components/Popup/Log.vue' 7 | import { select } from '@/components/Popup/Select' 8 | import { createPopupManager } from 'star-rail-vue' 9 | 10 | export const popupManager = createPopupManager({ 11 | loading: { component: Loading }, 12 | log: { component: Log }, 13 | data: { component: Data }, 14 | cropper, 15 | confirm, 16 | input, 17 | select 18 | }) 19 | 20 | popupManager.open('loading') 21 | -------------------------------------------------------------------------------- /src/assets/scripts/portraits.ts: -------------------------------------------------------------------------------- 1 | import { current, data, list } from '@/store/data' 2 | import { state } from '@/store/setting' 3 | import { emitter } from './event' 4 | import { popupManager } from './popup' 5 | 6 | export const startScreenshot = () => { 7 | if (state.window !== 'index') { 8 | state.expand = true 9 | state.window = 'index' 10 | } 11 | nextTick(() => { 12 | emitter.emit('screenshot') 13 | }) 14 | } 15 | 16 | export const showFirstItem = () => { 17 | for (const group of list.value.entries()) { 18 | if (group[1].length > 0) { 19 | state.group = group[0] 20 | state.ID = group[1][0].id 21 | break 22 | } 23 | } 24 | } 25 | 26 | export const typeEdit = (select?: string) => { 27 | return new Promise((resolve) => { 28 | popupManager 29 | .open('select', { 30 | title: '精怪分类', 31 | list: Array.from(list.value.keys()), 32 | select: select, 33 | extra: { 34 | name: '新增', 35 | fn: () => { 36 | popupManager 37 | .open('input', { 38 | title: '新增分类' 39 | }) 40 | .then((type) => { 41 | if (type !== null) { 42 | resolve(type) 43 | popupManager.close('select') 44 | } 45 | }) 46 | } 47 | } 48 | }) 49 | .then((type) => { 50 | if (type) { 51 | if (type) resolve(type) 52 | } 53 | }) 54 | }) 55 | } 56 | 57 | export const titleEdit = () => { 58 | if (!current.value) return 59 | popupManager 60 | .open('input', { 61 | title: '精怪名', 62 | placeholder: '???', 63 | defaultText: current.value.name, 64 | required: false 65 | }) 66 | .then((name) => { 67 | if (name !== null) current.value!.name = name 68 | }) 69 | } 70 | 71 | export const infoEdit = () => { 72 | if (!current.value) return 73 | popupManager 74 | .open('input', { 75 | title: '精怪简述', 76 | defaultText: current.value.info, 77 | textarea: true 78 | }) 79 | .then((info) => { 80 | if (info !== null) current.value!.info = info 81 | }) 82 | } 83 | 84 | export const textEdit = () => { 85 | if (!current.value) return 86 | popupManager 87 | .open('input', { 88 | title: '精怪详述', 89 | defaultText: current.value.text, 90 | textarea: true 91 | }) 92 | .then((text) => { 93 | if (text !== null) current.value!.text = text 94 | }) 95 | } 96 | 97 | export const imageEdit = () => { 98 | popupManager.open('cropper', { aspectRatio: 0.7, maxWidth: 1280 }).then((res) => { 99 | if (!current.value) return 100 | current.value.image = res.base64 101 | }) 102 | } 103 | 104 | export const deleteItem = () => { 105 | return new Promise((resolve) => { 106 | if (!current.value) return 107 | const id = current.value.id 108 | const key = current.value.type 109 | const index = data.list.findIndex((item) => item.id === id) 110 | if (id !== -1) { 111 | popupManager.open('confirm', { 112 | text: ['是否删除该影神图?'], 113 | fn: () => { 114 | data.list.splice(index, 1) 115 | const group = list.value.get(key) 116 | if (group && group.length === 0) { 117 | state.group = '' 118 | } 119 | resolve() 120 | } 121 | }) 122 | } 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /src/assets/scripts/update.ts: -------------------------------------------------------------------------------- 1 | import log from '@/assets/data/log' 2 | import { KEY } from '@/store/setting' 3 | import { timeComparison } from 'star-rail-vue' 4 | import { useRegisterSW } from 'virtual:pwa-register/vue' 5 | import type { WatchStopHandle } from 'vue' 6 | import { popupManager } from './popup' 7 | 8 | export const logCheck = () => { 9 | timeComparison(KEY.UPDATE_KEY, log[0]?.time).then(() => popupManager.open('log')) 10 | } 11 | 12 | const { needRefresh, updateServiceWorker } = useRegisterSW() 13 | 14 | export const updateCheck = () => { 15 | const updateWatcher: WatchStopHandle = watchEffect(() => { 16 | if (needRefresh.value) { 17 | nextTick(() => { 18 | updateWatcher() 19 | }) 20 | popupManager.open('confirm', { 21 | text: ['发现新版本,是否立刻更新?'], 22 | fn: () => { 23 | popupManager.open('loading') 24 | updateServiceWorker(true) 25 | } 26 | }) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/styles/function.styl: -------------------------------------------------------------------------------- 1 | //TODO 提取全部公共样式 2 | mask_image(url) 3 | position absolute 4 | z-index 1 5 | background url(url) 6 | background-position 100% 100% 7 | background-size 100% 8 | background-repeat no-repeat 9 | transition 0.3s 10 | mask-position 120% 0 11 | inset 0 12 | mask-image linear-gradient(to right, #000, #000, 50%, transparent 60%) 13 | mask-size 200% 100% 14 | mask-repeat no-repeat -------------------------------------------------------------------------------- /src/components/Common/Btn.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /src/components/Common/Icon.tsx: -------------------------------------------------------------------------------- 1 | export const Arrow = () => ( 2 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/components/Common/Keyboard.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 41 | -------------------------------------------------------------------------------- /src/components/Common/Window.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 45 | 46 | 79 | -------------------------------------------------------------------------------- /src/components/Index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /src/components/Index/Portraits.vue: -------------------------------------------------------------------------------- 1 | 124 | 125 | 201 | 202 | 468 | -------------------------------------------------------------------------------- /src/components/Index/Setting.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 222 | 223 | 320 | -------------------------------------------------------------------------------- /src/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 135 | 136 | 214 | -------------------------------------------------------------------------------- /src/components/Popup/Confirm/Confirm.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 61 | 62 | 83 | -------------------------------------------------------------------------------- /src/components/Popup/Confirm/data.ts: -------------------------------------------------------------------------------- 1 | export const data = reactive<{ 2 | text: string[] 3 | fn?: () => void 4 | close?: () => void 5 | }>({ 6 | text: [], 7 | fn: undefined 8 | }) 9 | 10 | let confirm = () => {} 11 | export const callback = { 12 | open: (config: typeof data) => { 13 | data.text = config.text 14 | data.fn = config.fn 15 | data.close = config.close 16 | }, 17 | close: () => { 18 | data.text = [] 19 | data.fn = undefined 20 | if (data.close) { 21 | data.close() 22 | data.close = undefined 23 | } 24 | }, 25 | set confirm(fn: () => any) { 26 | confirm = fn 27 | }, 28 | get confirm() { 29 | return () => { 30 | confirm() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Popup/Confirm/index.ts: -------------------------------------------------------------------------------- 1 | import Confirm from './Confirm.vue' 2 | import { data, callback } from './data' 3 | 4 | export const useConfirm = () => data 5 | 6 | export const confirm = { 7 | component: Confirm, 8 | ...callback 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Popup/Cropper/Cropper.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 95 | 96 | 119 | -------------------------------------------------------------------------------- /src/components/Popup/Cropper/data.ts: -------------------------------------------------------------------------------- 1 | import { imageCompress } from 'star-rail-vue' 2 | import { SuperImageCropper } from 'super-image-cropper' 3 | 4 | export const imageCropper = new SuperImageCropper() 5 | 6 | export const data = reactive<{ 7 | img: string 8 | aspectRatio?: number 9 | fn?: (img: string) => void 10 | }>({ 11 | img: '' 12 | }) 13 | 14 | const cropperOpen = (img: string, aspectRatio?: number) => { 15 | return new Promise((resolve) => { 16 | data.img = img 17 | data.aspectRatio = aspectRatio 18 | data.fn = (str) => resolve(str) 19 | }) 20 | } 21 | 22 | let confirm = () => {} 23 | export const callback = { 24 | open: (config?: { aspectRatio?: number; maxWidth?: number }) => { 25 | return new Promise<{ base64: string; raw: File }>((resolve) => { 26 | const el = document.createElement('input') 27 | el.type = 'file' 28 | el.accept = 'image/*' 29 | el.onchange = async () => { 30 | if (el.files?.[0]) { 31 | resolve({ 32 | base64: await cropperOpen( 33 | await imageCompress(el.files[0], config?.maxWidth), 34 | config?.aspectRatio 35 | ), 36 | raw: el.files[0] 37 | }) 38 | } 39 | } 40 | el.click() 41 | }) 42 | }, 43 | close: () => { 44 | data.img = '' 45 | data.aspectRatio = undefined 46 | data.fn = undefined 47 | }, 48 | set confirm(fn: () => any) { 49 | confirm = fn 50 | }, 51 | get confirm() { 52 | return () => { 53 | confirm() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Popup/Cropper/index.ts: -------------------------------------------------------------------------------- 1 | import Cropper from './Cropper.vue' 2 | import { data, callback } from './data' 3 | 4 | export const useCropper = () => data 5 | 6 | export const cropper = { 7 | component: Cropper, 8 | ...callback 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Popup/Data.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 88 | 89 | 148 | -------------------------------------------------------------------------------- /src/components/Popup/Input/Input.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 86 | 87 | 134 | -------------------------------------------------------------------------------- /src/components/Popup/Input/data.ts: -------------------------------------------------------------------------------- 1 | export const data = reactive<{ 2 | title: string 3 | required: boolean 4 | text: string 5 | placeholder?: string 6 | textarea?: boolean 7 | fn?: (str: string | null) => void 8 | }>({ 9 | title: '', 10 | required: true, 11 | text: '', 12 | placeholder: undefined, 13 | textarea: undefined, 14 | fn: undefined 15 | }) 16 | 17 | let confirm = () => {} 18 | export const callback = { 19 | open: (config: { 20 | title: string 21 | required?: boolean 22 | defaultText?: string 23 | placeholder?: string 24 | textarea?: boolean 25 | }) => { 26 | return new Promise((resolve) => { 27 | data.title = config.title 28 | data.required = config.required === undefined ? true : config.required 29 | if (config.defaultText) { 30 | data.text = config.defaultText 31 | } 32 | data.placeholder = config.placeholder 33 | data.textarea = config.textarea 34 | data.fn = (str: string | null) => { 35 | resolve(str) 36 | } 37 | }) 38 | }, 39 | close: () => { 40 | data.fn?.(null) 41 | data.title = '' 42 | data.required = true 43 | data.text = '' 44 | data.placeholder = undefined 45 | data.textarea = undefined 46 | data.fn = undefined 47 | }, 48 | set confirm(fn: () => any) { 49 | confirm = fn 50 | }, 51 | get confirm() { 52 | return () => { 53 | confirm() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Popup/Input/index.ts: -------------------------------------------------------------------------------- 1 | import Input from './Input.vue' 2 | import { data, callback } from './data' 3 | 4 | export const useInput = () => data 5 | 6 | export const input = { 7 | component: Input, 8 | ...callback 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Popup/Loading.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 71 | -------------------------------------------------------------------------------- /src/components/Popup/Log.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 83 | 84 | 162 | -------------------------------------------------------------------------------- /src/components/Popup/Select/Select.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 69 | 70 | 133 | -------------------------------------------------------------------------------- /src/components/Popup/Select/data.ts: -------------------------------------------------------------------------------- 1 | export const data = reactive<{ 2 | title: string 3 | list: string[] | readonly string[] 4 | select?: string 5 | extra?: { name: string; fn: () => void } 6 | fn?: () => void 7 | }>({ 8 | title: '', 9 | list: [], 10 | select: undefined, 11 | fn: undefined 12 | }) 13 | 14 | let confirm = () => {} 15 | export const callback = { 16 | open: (config: { 17 | title: string 18 | list: T 19 | select?: T[number] 20 | extra?: { name: string; fn: () => void } 21 | }) => { 22 | return new Promise((resolve) => { 23 | data.title = config.title 24 | data.list = config.list 25 | data.select = config.select 26 | data.extra = config.extra 27 | data.fn = () => { 28 | resolve(data.select) 29 | } 30 | }) 31 | }, 32 | close: () => { 33 | data.title = '' 34 | data.list = [] 35 | data.select = undefined 36 | data.extra = undefined 37 | data.fn = undefined 38 | }, 39 | set confirm(fn: () => any) { 40 | confirm = fn 41 | }, 42 | get confirm() { 43 | return () => { 44 | confirm() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Popup/Select/index.ts: -------------------------------------------------------------------------------- 1 | import Select from './Select.vue' 2 | import { callback, data } from './data' 3 | 4 | export const useSelect = () => data 5 | 6 | export const select = { 7 | component: Select, 8 | ...callback 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Shadow.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/components/Tip.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 27 | 28 | 76 | -------------------------------------------------------------------------------- /src/main.styl: -------------------------------------------------------------------------------- 1 | :root 2 | --text-color #c8c7c6 3 | 4 | @keyframes first 5 | from 6 | filter blur(10px) 7 | opacity 0 8 | 9 | html 10 | font-size 36px 11 | animation first 0.5s 12 | 13 | body 14 | margin 0 15 | background-color #000 16 | 17 | &::-webkit-scrollbar 18 | width 0px 19 | height 0px 20 | 21 | * 22 | user-select none 23 | -webkit-user-drag none 24 | user-drag none 25 | 26 | ::-webkit-scrollbar 27 | width 0px 28 | height 0px 29 | 30 | .hover 31 | &:hover 32 | filter brightness(1.3) 33 | 34 | .ellipsis 35 | overflow hidden 36 | text-overflow ellipsis 37 | white-space nowrap 38 | 39 | .fade-in-enter-active 40 | transition all 0.2s 41 | 42 | .fade-in-enter-from 43 | opacity 0 44 | 45 | @keyframes backdrop-filter 46 | from 47 | backdrop-filter blur(0px) 48 | 49 | to 50 | backdrop-filter blur(10px) -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './main.styl' 2 | 3 | import { analytics } from 'star-rail-vue' 4 | import VueDOMPurifyHTML from 'vue-dompurify-html' 5 | import App from './App.vue' 6 | import { loadDatabase } from './assets/scripts/database' 7 | import { hotkey } from './assets/scripts/hotkey' 8 | import { showFirstItem } from './assets/scripts/portraits' 9 | import { logCheck, updateCheck } from './assets/scripts/update' 10 | 11 | analytics('G-10B19F2P0B', import.meta.env.MODE === 'development') 12 | 13 | createApp(App) 14 | .use(VueDOMPurifyHTML, { 15 | allowedTags: ['br', 'span'], 16 | allowedAttributes: { 17 | span: ['style', 'class'] 18 | } 19 | }) 20 | .mount('#app') 21 | 22 | hotkey() 23 | logCheck() 24 | loadDatabase().then(() => { 25 | showFirstItem() 26 | updateCheck() 27 | }) 28 | -------------------------------------------------------------------------------- /src/store/data.ts: -------------------------------------------------------------------------------- 1 | import { state } from './setting' 2 | 3 | export const current = computed(() => { 4 | const index = data.list.findIndex((item) => item.id === state.ID) 5 | if (index === -1) return 6 | return data.list[index] 7 | }) 8 | 9 | export const list = computed(() => { 10 | const _list: Map = new Map([ 11 | ['小妖', []], 12 | ['头目', []], 13 | ['妖王', []], 14 | ['人物', []] 15 | ]) 16 | data.list.forEach((item) => { 17 | if (!_list.has(item.type)) { 18 | _list.set(item.type, [item]) 19 | } else { 20 | _list.get(item.type)!.push(item) 21 | } 22 | }) 23 | for (const group of _list.values()) { 24 | group.sort((a, b) => b.time - a.time) 25 | } 26 | return _list 27 | }) 28 | 29 | export const data = reactive<{ 30 | list: Portrait[] 31 | }>({ 32 | list: [] 33 | }) 34 | -------------------------------------------------------------------------------- /src/store/setting.ts: -------------------------------------------------------------------------------- 1 | import { setLocalStorage } from 'star-rail-vue' 2 | 3 | export const KEY = { 4 | DATABASE_NAME: 'wukong-portraits', 5 | SETTING_KEY: 'wukong-portraits-setting', 6 | FILE_ACCEPT: '.wukong', 7 | RAW_NAME: 'raw.wukong', 8 | UPDATE_KEY: 'wukong-portraits-update' 9 | } 10 | 11 | export const state: { 12 | ID?: number 13 | group: string 14 | expand: boolean 15 | window: 'index' | 'setting' 16 | screenshot: boolean 17 | } = reactive({ 18 | ID: undefined, 19 | group: '', 20 | expand: false, 21 | window: 'index', 22 | screenshot: false 23 | }) 24 | 25 | export const setting: { 26 | download: boolean 27 | quality: number 28 | tip: boolean 29 | } = reactive({ 30 | download: true, 31 | quality: 1, 32 | tip: true 33 | }) 34 | 35 | setLocalStorage(setting, KEY.SETTING_KEY) 36 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | interface Portrait { 2 | id: number 3 | name: string 4 | type: string 5 | info: string 6 | text: string 7 | image?: string 8 | overlay?: boolean 9 | time: number 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "auto-imports.d.ts", 5 | "env.d.ts", 6 | "src/**/*", 7 | "src/**/*.vue", 8 | "src/**/*.json" 9 | ], 10 | "exclude": [ 11 | "src/**/__tests__/*" 12 | ], 13 | "compilerOptions": { 14 | "composite": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": [ 18 | "./src/*" 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": [ 15 | "node" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import vue from '@vitejs/plugin-vue' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import { buildTime } from 'star-rail-vue/vite' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import { defineConfig } from 'vite' 8 | import { VitePWA } from 'vite-plugin-pwa' 9 | import VueDevTools from 'vite-plugin-vue-devtools' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | plugins: [ 14 | buildTime(), 15 | vue({ 16 | script: { 17 | defineModel: true 18 | } 19 | }), 20 | vueJsx(), 21 | AutoImport({ 22 | imports: ['vue'] 23 | }), 24 | VueDevTools(), 25 | VitePWA({ 26 | mode: 'production', 27 | injectRegister: 'auto', 28 | registerType: 'prompt', 29 | manifest: { 30 | id: '/', 31 | name: '黑神话:悟空 - 影神图', 32 | short_name: '影神图', 33 | description: '黑神话:悟空影神图生成器', 34 | display: 'fullscreen', 35 | orientation: 'landscape', 36 | theme_color: '#000', 37 | background_color: '#000', 38 | lang: 'zh-cn', 39 | icons: [ 40 | { 41 | src: 'icon.webp', 42 | type: 'image/webp', 43 | sizes: '256x256' 44 | } 45 | ], 46 | screenshots: [ 47 | { 48 | src: 'preview.webp', 49 | sizes: '1000x565' 50 | }, 51 | { 52 | src: 'preview.webp', 53 | sizes: '1000x565', 54 | form_factor: 'wide' 55 | } 56 | ] 57 | }, 58 | workbox: { 59 | // skipWaiting: true, 60 | disableDevLogs: true, 61 | runtimeCaching: [ 62 | { 63 | urlPattern: /(.*?)\.(png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps|ico|webp)/i, 64 | handler: 'CacheFirst', 65 | options: { 66 | cacheName: 'image-cache' 67 | } 68 | }, 69 | { 70 | urlPattern: /(.*?)\.(woff2)/i, 71 | handler: 'CacheFirst', 72 | options: { 73 | cacheName: 'font-cache' 74 | } 75 | } 76 | ] 77 | }, 78 | devOptions: { 79 | enabled: true, 80 | suppressWarnings: true 81 | } 82 | }) 83 | ], 84 | resolve: { 85 | alias: { 86 | '@': fileURLToPath(new URL('./src', import.meta.url)) 87 | } 88 | }, 89 | build: { 90 | rollupOptions: { 91 | output: { 92 | manualChunks: { 93 | vue: ['vue'], 94 | sr: ['star-rail-vue'] 95 | } 96 | } 97 | }, 98 | assetsInlineLimit: 1024 * 200, 99 | chunkSizeWarningLimit: 1024 100 | } 101 | }) 102 | --------------------------------------------------------------------------------