├── .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 | 
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 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
14 |
15 |
16 |
22 |
23 |
58 |
--------------------------------------------------------------------------------
/src/components/Common/Icon.tsx:
--------------------------------------------------------------------------------
1 | export const Arrow = () => (
2 |
11 | )
12 |
--------------------------------------------------------------------------------
/src/components/Common/Keyboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ keyboard }}
4 |
{{ text }}
5 |
6 |
7 |
8 |
14 |
15 |
41 |
--------------------------------------------------------------------------------
/src/components/Common/Window.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
21 |
22 |
23 |
24 |
45 |
46 |
79 |
--------------------------------------------------------------------------------
/src/components/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
26 |
27 |
35 |
--------------------------------------------------------------------------------
/src/components/Index/Portraits.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
影神图
12 |
13 |
19 |
23 |
24 |
{{ group[0] }}
25 |
26 |
27 |
28 |
37 |
38 |
{{ item.name }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
51 |
58 |
{{ current.name }}
63 |
64 |
65 |
72 |
77 | ……
78 |
79 |
86 |
91 | ……
92 |
93 |
98 |
99 |
104 |
108 |
![]()
113 |
114 |
119 | {{ current.overlay ? '还原' : '融入' }}
120 |
121 |
122 |
123 |
124 |
125 |
201 |
202 |
468 |
--------------------------------------------------------------------------------
/src/components/Index/Setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | {{ group.label }}
9 |
13 |
19 |
{{ item.key }}
20 |
21 |
22 |
28 | {{ item.value }}
29 |
35 |
36 |
37 |
43 | {{ item.value }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
55 |
{{ currentMenu.key }}
56 |
60 |
61 |
62 |
63 |
64 |
222 |
223 |
320 |
--------------------------------------------------------------------------------
/src/components/Menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
13 |
14 |
15 |
19 |
23 |
35 |
42 | {{ item.name }}
43 |
44 |
45 |
46 |
47 |
48 |
135 |
136 |
214 |
--------------------------------------------------------------------------------
/src/components/Popup/Confirm/Confirm.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
32 |
33 |
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 |
2 |
3 |
4 |
10 |
22 |
39 |
40 |
41 |
42 |
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 |
2 |
6 |
20 |
21 |
22 |
23 |
88 |
89 |
148 |
--------------------------------------------------------------------------------
/src/components/Popup/Input/Input.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
38 |
39 |
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 |
2 |
20 |
21 |
22 |
28 |
29 |
71 |
--------------------------------------------------------------------------------
/src/components/Popup/Log.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
62 |
63 |
64 |
65 |
83 |
84 |
162 |
--------------------------------------------------------------------------------
/src/components/Popup/Select/Select.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
{{ data.title }}
8 |
28 |
39 |
40 |
41 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/src/components/Tip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
15 |
16 |
17 |
影神图的内容均可修改
18 |
19 |
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------