├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── src ├── config │ └── index.ts ├── utils │ ├── axios │ │ ├── 为了达到通用,不建议直接修改此文件夹内容 │ │ ├── AxiosRequestExt.ts │ │ ├── example │ │ │ ├── exampleIndex.ts │ │ │ └── exampleInterceptorHooks.ts │ │ ├── types.ts │ │ └── AxiosRequest.ts │ ├── progress │ │ └── index.ts │ └── utils.ts ├── style │ ├── main.scss │ ├── elememtui-plus.scss │ └── base.scss ├── assets │ ├── md │ │ ├── img_home.png │ │ ├── img_update.png │ │ ├── img_generate-step-1.png │ │ └── img_generate-step-final.png │ ├── svg │ │ ├── arrow.svg │ │ └── github.svg │ └── logo.svg ├── App.vue ├── types │ ├── vue-files.d.ts │ ├── components.d.ts │ └── auto-imports.d.ts ├── main.ts ├── stores │ ├── index.ts │ └── modules │ │ ├── imaotai.ts │ │ └── user.ts ├── router │ └── index.ts ├── api │ ├── http │ │ ├── index.ts │ │ └── customInterceptorHooks.ts │ ├── other │ │ └── apple.ts │ └── imaotai │ │ ├── user.ts │ │ └── shop.ts ├── components │ └── CommonHeader.vue └── views │ ├── generate │ ├── components │ │ ├── StepTwo.vue │ │ ├── StepFinal.vue │ │ ├── StepThree.vue │ │ └── StepOne.vue │ └── index.vue │ ├── home │ └── index.vue │ └── update │ └── index.vue ├── .npmrc ├── env.d.ts ├── .yarnrc.yml ├── public └── favicon.ico ├── .dockerignore ├── .dev.vars ├── tsconfig.node.json ├── .stylelintignore ├── server ├── package.json └── app.js ├── lint-staged.config.js ├── postcss.config.js ├── index.html ├── wrangler.toml ├── .env ├── .env.example ├── tailwind.config.js ├── .editorconfig ├── .gitignore ├── eslint.config.js ├── tsconfig.json ├── stylelint.config.js ├── commitlint.config.js ├── functions ├── mtstaticapi │ └── [[path]].js ├── mtappapi │ └── [[path]].js └── appleapi │ └── [[path]].js ├── .github └── workflows │ └── docker-image.yml ├── Dockerfile ├── package.json ├── vite.config.ts ├── README.md └── LICENSE /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/utils/axios/为了达到通用,不建议直接修改此文件夹内容: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]="@vue/runtime-core" 2 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/style/main.scss: -------------------------------------------------------------------------------- 1 | @use './base' as *; 2 | @use './elememtui-plus' as *; 3 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkenClub/iMoutaiEnvGenerator/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | server/node_modules 3 | .git 4 | .gitignore 5 | *.md 6 | .husky 7 | .vscode 8 | .idea -------------------------------------------------------------------------------- /src/assets/md/img_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkenClub/iMoutaiEnvGenerator/HEAD/src/assets/md/img_home.png -------------------------------------------------------------------------------- /src/assets/md/img_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkenClub/iMoutaiEnvGenerator/HEAD/src/assets/md/img_update.png -------------------------------------------------------------------------------- /src/assets/md/img_generate-step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkenClub/iMoutaiEnvGenerator/HEAD/src/assets/md/img_generate-step-1.png -------------------------------------------------------------------------------- /src/assets/md/img_generate-step-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkenClub/iMoutaiEnvGenerator/HEAD/src/assets/md/img_generate-step-final.png -------------------------------------------------------------------------------- /src/style/elememtui-plus.scss: -------------------------------------------------------------------------------- 1 | @forward 'element-plus/theme-chalk/el-message.css'; 2 | @forward 'element-plus/theme-chalk/el-message-box.css'; 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # 经过 pre-commit 后,通过 commitlint 对提交的信息进行检查 5 | npx --no-install commitlint --edit "$1" 6 | -------------------------------------------------------------------------------- /.dev.vars: -------------------------------------------------------------------------------- 1 | # .dev.vars 2 | VITE_APP_STORE_URL="https://apps.apple.com" 3 | VITE_MT_SHOP_STATIC_URL="https://static.moutai519.com.cn" 4 | VITE_MT_APP_API_URL="https://app.moutai519.com.cn" -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "types": ["node"] 6 | }, 7 | "include": ["vite.config.*"] 8 | } 9 | -------------------------------------------------------------------------------- /src/types/vue-files.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # .stylelintignore 2 | # 旧的不需打包的样式库 3 | *.min.css 4 | 5 | # 其他类型文件 6 | *.js 7 | *.jpg 8 | *.png 9 | *.eot 10 | *.ttf 11 | *.woff 12 | *.json 13 | 14 | # 测试和打包目录 15 | /test/ 16 | /dist/ 17 | /node_modules/ 18 | /lib/ 19 | /public/* 20 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imaotai-env-generator-server", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "express": "4.21.2", 8 | "http-proxy-middleware": "3.0.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/svg/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * lint-staged 是一个专门用于在通过 git 提交代码之前,对暂存区的代码执行一系列的格式化。 3 | * 当 lint-staged 配合 git hooks 使用时,可以在 git 提交前的 hook 中加入 lint-staged 命令, 4 | * 这样就能在提交代码之前,对即将提交的代码进行格式化,成功之后就会提交代码 5 | * */ 6 | 7 | export default { 8 | '*.{vue,css,scss,postcss,less,html}': ['stylelint --fix'], 9 | '*': 'eslint --fix', 10 | } 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { setupStore } from '@/stores' 2 | import { createApp } from 'vue' 3 | import App from './App.vue' 4 | 5 | import router from './router' 6 | import 'tailwindcss/tailwind.css' 7 | 8 | import './style/main.scss' 9 | 10 | const app = createApp(App) 11 | 12 | app.use(router) 13 | setupStore(app) 14 | 15 | app.mount('#app') 16 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PostCSS配置,vite 会默认使用此配置 3 | * https://cn.vite.dev/guide/features#postcss 4 | */ 5 | export default { 6 | plugins: { 7 | // 可以不用 cssnano,因为 vite 默认压缩 css。 8 | // 增加 tailwindcss 才会编译 vue 等文件中 tailwindcss 的 class。 9 | tailwindcss: {}, 10 | // autoprefixer 会根据 browsers 选项添加浏览器前缀。 11 | autoprefixer: {}, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/progress/index.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import 'nprogress/nprogress.css' 3 | 4 | NProgress.configure({ 5 | // 动画方式 6 | easing: 'ease', 7 | // 递增进度条的速度 8 | speed: 500, 9 | // 是否显示加载ico 10 | showSpinner: false, 11 | // 自动递增间隔 12 | trickleSpeed: 200, 13 | // 初始化时的最小百分比 14 | minimum: 0.3, 15 | }) 16 | 17 | export default NProgress 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 4 | 5 | const pinia = createPinia() 6 | // 使用持久化插件:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/why.html 7 | pinia.use(piniaPluginPersistedstate) 8 | 9 | export function setupStore(app: App) { 10 | app.use(pinia) 11 | } 12 | 13 | export { pinia } 14 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "imaotai-env-generator" # 您可以根据 Cloudflare 上的项目名调整 2 | compatibility_date = "2025-05-05" # 建议使用较新的日期,例如今天或您开始使用 Wrangler 的日期 3 | pages_build_output_dir = "dist" 4 | 5 | [vars] 6 | VITE_APP_STORE_URL = "https://apps.apple.com" 7 | VITE_MT_SHOP_STATIC_URL = "https://static.moutai519.com.cn" 8 | VITE_MT_APP_API_URL = "https://app.moutai519.com.cn" 9 | 10 | # 如果您的 Functions 依赖 Node.js 特定 API,可以取消注释并设置 11 | # node_compat = true 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # _/husky.sh 会在 package.json - script - prepare 中自动生成 3 | . "$(dirname "$0")/_/husky.sh" 4 | 5 | # 对应 package.json - script 中 lint-staged 命令,配合项目中 .lintstagedrc.js 规则 6 | npm run lint:lint-staged 7 | 8 | # --- 📘 介绍 --- 9 | # 可以在项目中植入你设定的 git hooks,在 git 提交代码的前后,你预设的 git hooks 可以得到执行, 10 | # 以对代码、文件等进行预设的检查,一旦检查不通过,就可以阻止当前的代码提交,避免了不规范的代码和 git 提交出现在项目中。 11 | # git hooks 是本地的,不会被同步到 git 仓库里。为了保证每个人的本地仓库都能执行预设的 git hooks,于是就有了 husky。 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_PROXY_BASE_URL='https://example.com' 2 | 3 | # 苹果应用商店地址 4 | VITE_APP_STORE_URL='https://apps.apple.com' 5 | # 苹果接口地址前缀 6 | VITE_APP_STORE_API_PREFIX='/appleapi' 7 | 8 | # 茅台商城静态资源地址 9 | VITE_MT_SHOP_STATIC_URL='https://static.moutai519.com.cn' 10 | # 茅台商城静态资源接口地址前缀 11 | VITE_MT_SHOP_STATIC_API_PREFIX='/mtstaticapi' 12 | 13 | # 茅台 APP 接口地址 14 | VITE_MT_APP_API_URL='https://app.moutai519.com.cn' 15 | # 茅台 APP 接口地址前缀 16 | VITE_MT_APP_API_PREFIX='/mtappapi' 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # .env.example 2 | # 这些是 Cloudflare Pages Functions 代理所需的目标 API 的基础 URL。 3 | # 请在 Cloudflare Pages 项目的环境变量设置中配置这些值, 4 | # 或者在本地开发时,复制此文件为 .dev.vars 并填写真实值。 5 | 6 | # 苹果商店 API 代理的目标 URL 7 | VITE_APP_STORE_URL="https://apps.apple.com" 8 | 9 | # 茅台商城静态资源 API 代理的目标 URL 10 | VITE_MT_SHOP_STATIC_URL="https://static.moutai519.com.cn" 11 | 12 | # 茅台 APP API 代理的目标 URL 13 | VITE_MT_APP_API_URL="https://app.moutai519.com.cn" 14 | 15 | # 如果有其他需要的环境变量,也在这里列出 16 | # OPTIONAL_SETTING="some_default_or_placeholder" -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: { 6 | // 拓展颜色,使用:text-soft, 7 | // https://www.tailwindcss.cn/docs/customizing-colors#using-custom-colors 8 | colors: { 9 | soft: 'var(--vt-c-white-soft)', 10 | }, 11 | // 拓展高度,使用:h-600 12 | height: { 13 | 600: '600px', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.sass] 14 | indent_size = 2 15 | 16 | [*.scss] 17 | indent_size = 2 18 | 19 | [{*.ats,*.cts,*.mts,*.ts}] 20 | indent_size = 2 21 | tab_width = 2 22 | 23 | [{*.cjs,*.js}] 24 | indent_size = 2 25 | tab_width = 2 26 | 27 | [{*.nvue,*.vue}] 28 | indent_size = 2 29 | tab_width = 2 30 | -------------------------------------------------------------------------------- /.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 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | # Yarn files 31 | .yarn/* 32 | !.yarn/releases 33 | !.yarn/plugins 34 | !.yarn/sdks 35 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | const router = createRouter({ 4 | history: createWebHistory(import.meta.env.BASE_URL), 5 | routes: [ 6 | { 7 | path: '/', 8 | name: 'home', 9 | component: () => import('@/views/home/index.vue'), 10 | }, 11 | { 12 | path: '/generate', 13 | name: 'generate', 14 | component: () => import('@/views/generate/index.vue'), 15 | }, 16 | { 17 | path: '/update', 18 | name: 'update', 19 | component: () => import('@/views/update/index.vue'), 20 | }, 21 | ], 22 | }) 23 | 24 | export default router 25 | -------------------------------------------------------------------------------- /src/assets/svg/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/api/http/index.ts: -------------------------------------------------------------------------------- 1 | // 定义一个常见后端请求返回 2 | import { customInterceptorHooks } from '@/api/http/customInterceptorHooks' 3 | import { AxiosRequestExt } from '@/utils/axios/AxiosRequestExt' 4 | import { ElMessage } from 'element-plus' 5 | 6 | // 具体使用时先实例一个请求对象 7 | export const axiosRequest = new AxiosRequestExt({ 8 | timeout: 30000, 9 | headers: { 10 | 'Content-Type': 'application/x-www-form-urlencoded', 11 | }, 12 | requestOptions: { 13 | globalSuccessMessage: false, 14 | globalErrorMessage: true, 15 | globalErrorMessageHandle: (errorMessage?: string) => { 16 | ElMessage.error({ 17 | message: errorMessage || '未知错误', 18 | }) 19 | }, 20 | }, 21 | interceptorHooks: customInterceptorHooks(), 22 | }) 23 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function downloadEvt(url: string, fileName = '未知文件') { 2 | const el = document.createElement('a') 3 | el.style.display = 'none' 4 | el.setAttribute('target', '_blank') 5 | /** 6 | * download的属性是HTML5新增的属性 7 | * href属性的地址必须是非跨域的地址,如果引用的是第三方的网站或者说是前后端分离的项目(调用后台的接口),这时download就会不起作用。 8 | * 此时,如果是下载浏览器无法解析的文件,例如.exe,.xlsx..那么浏览器会自动下载,但是如果使用浏览器可以解析的文件,比如.txt,.png,.pdf....浏览器就会采取预览模式 9 | * 所以,对于.txt,.png,.pdf等的预览功能我们就可以直接不设置download属性(前提是后端响应头的Content-Type: application/octet-stream,如果为application/pdf浏览器则会判断文件为 pdf ,自动执行预览的策略) 10 | */ 11 | fileName && el.setAttribute('download', fileName) 12 | el.href = url 13 | console.log(el) 14 | document.body.append(el) 15 | el.click() 16 | el.remove() 17 | } 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | /** 4 | * @description eslint配置 5 | * https://github.com/antfu/eslint-config 6 | * 7 | * 需要 IDE 开启 ESLint 插件,不用 prettier 配置,禁用保存 format 相关插件,使用 ESLint 检查。 8 | * 一般 IDE 的 ESLint 默认没有检查 scss 等格式文件,可以按需加上 9 | */ 10 | export default antfu({ 11 | // `.eslintignore` is no longer supported in Flat config, use `ignores` instead 12 | ignores: ['src/typings', 'src/typings/**'], 13 | // Use external formatters to format files that ESLint cannot handle yet (.css, .html, etc). 14 | formatters: true, 15 | // Or customize the stylistic rules 16 | stylistic: { 17 | // 格式化的更多配置 https://eslint.style/rules 18 | }, 19 | // TypeScript and Vue are auto-detected, you can also explicitly enable them: 20 | typescript: true, 21 | vue: true, 22 | // 额外配置 23 | rules: { 24 | 'no-console': 'off', 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "lib": ["ESNext", "DOM"], 7 | "useDefineForClassFields": true, 8 | "baseUrl": ".", 9 | "module": "ESNext", 10 | "moduleResolution": "Node", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | }, 14 | "resolveJsonModule": true, 15 | "types": ["vite/client", "element-plus/global", "unplugin-icons/types/vue"], 16 | "strict": true, 17 | "removeComments": true, 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "isolatedModules": true, 21 | "skipLibCheck": true 22 | }, 23 | "references": [ 24 | { 25 | "path": "./tsconfig.node.json" 26 | } 27 | ], 28 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/typings/*.d.ts"], 29 | "exclude": ["node_modules", "dist", "**/*.js"] 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/axios/AxiosRequestExt.ts: -------------------------------------------------------------------------------- 1 | import type { ExpandInternalAxiosRequestConfig } from '@/utils/axios/types' 2 | import AxiosRequest from '@/utils/axios/AxiosRequest' 3 | 4 | /** 5 | * 对基本的 AxiosRequest 做拓展,补充 请求重连机制 6 | */ 7 | export class AxiosRequestExt extends AxiosRequest { 8 | /** token过期后,暂存待执行的请求 */ 9 | public requestsCache: Array<() => Promise> = [] 10 | 11 | /** 防止重复刷新token */ 12 | public isRefreshingToken = false 13 | 14 | /** 重连原始请求,一般用于 Token / Cookie 失效后,原请求失败需要在授权后重新发起请求 */ 15 | public retryOriginalRequest(config: ExpandInternalAxiosRequestConfig) { 16 | return new Promise((resolve) => { 17 | this.requestsCache.push(async () => { 18 | // 等待 token 被刷新后回调,用原请求的配置重新发起请求 19 | resolve(this.request(config)) 20 | }) 21 | }) 22 | } 23 | 24 | /** 25 | * 刷新 Token 成功后,执行缓存的请求队列 26 | */ 27 | public executeCachedRequests(): void { 28 | const cachedRequests = [...this.requestsCache] 29 | this.requestsCache = [] // 清空缓存 30 | cachedRequests.forEach(request => request()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/axios/example/exampleIndex.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | /** 4 | * 📢 一般不用动此文件 5 | * 示例,按照项目实际情况编写 6 | */ 7 | /* 8 | import Request from './Request' 9 | import { templateInterceptorHooks } from './templateInterceptorHooks' 10 | 11 | // 定义一个常见后端请求返回 12 | export type BaseApiResponse = { 13 | code: number 14 | message: string 15 | result: T 16 | } 17 | 18 | // 具体使用时先实例一个请求对象 19 | const request = new Request({ 20 | // baseURL: '/api', 21 | requestOptions: { 22 | globalErrorMessage: true, 23 | globalSuccessMessage: false 24 | }, 25 | interceptorHooks: templateInterceptorHooks 26 | }) 27 | 28 | export default request 29 | */ 30 | 31 | /* 32 | // 用法 33 | interface ResModel { 34 | str: string 35 | num: number 36 | } 37 | // 发起请求 38 | request 39 | .post( 40 | '/abc', 41 | { 42 | a: 'aa', 43 | }, 44 | { 45 | requestOptions: { 46 | globalErrorMessage: true, 47 | }, 48 | } 49 | ) 50 | .then((res) => { 51 | console.log('res: ', res) 52 | console.log(res.str) 53 | }) 54 | */ 55 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts', '**/*.json'], 3 | extends: [ 4 | // Stylelint 标准共享配置 5 | 'stylelint-config-standard', 6 | // 扩展 stylelint-config-recommended 共享配置并为 SCSS 配置其规则 7 | 'stylelint-config-standard-scss', 8 | // 扩展 stylelint-config-recommended 共享配置并为 Vue 配置其规则 9 | 'stylelint-config-standard-vue/scss', 10 | // 提供优化样式顺序的配置,https://github.com/stormwarning/stylelint-config-recess-order 11 | 'stylelint-config-recess-order', 12 | // 基于 prettier 代码风格的 stylelint 规则 13 | 'stylelint-prettier/recommended', 14 | // stylelint15 已经不用设置 'stylelint-config-prettier', 15 | ], 16 | // 指定不同文件对应的解析器 17 | overrides: [ 18 | { 19 | files: ['**/*.{vue,html}'], 20 | customSyntax: 'postcss-html', 21 | }, 22 | { 23 | files: ['**/*.{css,scss}'], 24 | customSyntax: 'postcss-scss', 25 | }, 26 | ], 27 | plugins: [], 28 | // 自定义规则 29 | rules: { 30 | // 不检查类名命名规则,兼容 BEM 形式 31 | 'selector-class-pattern': null, 32 | // 兼容小程序单位 33 | 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }], 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /src/api/other/apple.ts: -------------------------------------------------------------------------------- 1 | import { axiosRequest } from '@/api/http' 2 | 3 | const APPLE_API_PRE_FIX = import.meta.env.VITE_APP_STORE_API_PREFIX 4 | 5 | /** 6 | * 从苹果应用商店获取茅台应用的版本号 7 | * @returns Promise 返回版本号 8 | * @throws Error 当无法获取版本号时抛出异常 9 | */ 10 | export async function getMtAppVersion(): Promise { 11 | try { 12 | const response = await axiosRequest.get(`${APPLE_API_PRE_FIX}/cn/app/i%E8%8C%85%E5%8F%B0/id1600482450`) 13 | 14 | // 使用正则表达式匹配版本号,优先匹配原有格式 15 | let pattern = /new__latest__version">(.*?)<\/p>/ 16 | let match = response.match(pattern) 17 | 18 | // 兼容:如果原有格式未匹配到,尝试匹配新格式

版本 1.8.5

19 | if (!match) { 20 | pattern = /]*>版本\s+(\d+\.\d+\.\d+)<\/h4>/ 21 | match = response.match(pattern) 22 | } 23 | 24 | if (match && match[1]) { 25 | // 清理版本号字符串,去除"版本 "前缀和空白字符 26 | const mtVersion = match[1].trim().replace('版本 ', '') 27 | return mtVersion 28 | } 29 | throw new Error('获取版本号失败:未找到版本信息') 30 | } 31 | catch (error: any) { 32 | throw new Error(`获取版本号失败:${error?.message || '未知错误'}`) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // commitlint 是一个 git commit 校验约束工具。检查提交的信息是否符合标准 2 | // ✨提交说明的结构如下所示: 3 | // <类型>([可选的作用域]): <描述> 4 | // 例如:feat(login): add login function 5 | // 6 | // [可选的正文] 7 | // 8 | // [可选的脚注] 9 | // ----------------- 10 | export default { 11 | extends: [ 12 | // https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional 13 | '@commitlint/config-conventional', 14 | ], 15 | rules: { 16 | // 可自定义规则,会覆盖上面 extends 对应项 17 | 'type-enum': [ 18 | 2, 19 | 'always', 20 | [ 21 | // 新特性、新功能 22 | 'feat', 23 | // 修复 bug 24 | 'fix', 25 | // 优化相关,比如提升性能、体验 26 | 'perf', 27 | // 样式相关 28 | 'style', 29 | // 文档修改 30 | 'docs', 31 | // 添加确实测试或更正现有的测试 32 | 'test', 33 | // 代码重构,既不修复错误也不添加功能 34 | 'refactor', 35 | // 编译相关的修改,例如发布版本、对项目构建或者依赖的改动 36 | 'build', 37 | // 自动化流程配置或脚本修改 38 | 'ci', 39 | // 其他修改, 比如改变构建流程、或者增加依赖库、工具等 40 | 'chore', 41 | // 回滚到上一个版本 42 | 'revert', 43 | // 版本发布 44 | 'release', 45 | ], 46 | ], 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/axios/types.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' 2 | 3 | /** 4 | * 📢 一般不用动此文件 5 | */ 6 | 7 | // 拓展 axios 请求配置,加入我们自己的配置 8 | interface RequestOptions { 9 | // 是否全局展示请求 错误信息 10 | globalErrorMessage?: boolean 11 | // 全局展示请求的 错误信息 的处理方法 12 | globalErrorMessageHandle?: (errorMessage?: string) => void 13 | // 是否全局展示请求 成功信息 14 | globalSuccessMessage?: boolean 15 | // 是否全局展示请求 成功信息 的处理方法 16 | globalSuccessMessageHandle?: (successMessage?: string) => void 17 | // 是否跳过响应拦截器 18 | skipResponseInterceptor?: boolean 19 | } 20 | 21 | // 定义拦截器 22 | export interface InterceptorHooks { 23 | requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig 24 | requestInterceptorCatch?: (error: any) => any 25 | responseInterceptor?: (response: AxiosResponse) => AxiosResponse | Promise 26 | responseInterceptorCatch?: (error: any) => any 27 | } 28 | 29 | // 拓展自定义请求配置 30 | export interface ExpandAxiosRequestConfig extends AxiosRequestConfig { 31 | interceptorHooks?: InterceptorHooks 32 | requestOptions?: RequestOptions 33 | } 34 | 35 | // 拓展 axios 请求配置 36 | export interface ExpandInternalAxiosRequestConfig extends InternalAxiosRequestConfig { 37 | interceptorHooks?: InterceptorHooks 38 | requestOptions?: RequestOptions 39 | } 40 | 41 | // 拓展 axios 返回配置 42 | export interface ExpandAxiosResponse extends AxiosResponse { 43 | config: ExpandInternalAxiosRequestConfig 44 | } 45 | -------------------------------------------------------------------------------- /src/stores/modules/imaotai.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { reactive } from 'vue' 3 | 4 | // 配置环境变量:KEN_IMAOTAI_ENV 5 | // 直接使用 `#` 作为分隔符即可 6 | // 内容格式为:PHONE_NUMBER#USER_ID#DEVICE_ID#MT_VERSION#PRODUCT_ID_LIST#SHOP_ID^SHOP_MODE^PROVINCE^CITY#LAT#LNG#TOKEN#COOKIE 7 | // 解释:手机号码#用户ID#设备ID#版本号#商品ID列表#店铺ID店铺缺货时自动采用的模式^省份^城市#纬度#经度#TOKEN#COOKIE 8 | // 多个用户时使用 & 连接。 9 | 10 | export interface ImaotaiState { 11 | phoneNumber: string 12 | userId: string 13 | deviceId: string 14 | mtVersion: string 15 | productIdList: string 16 | shopId: string 17 | shopMode: string 18 | province: string 19 | city: string 20 | lat: string 21 | lng: string 22 | token: string 23 | cookie: string 24 | } 25 | 26 | export const useImaotaiStore = defineStore('imaotai', () => { 27 | const state = reactive({ 28 | phoneNumber: '', 29 | userId: '', 30 | deviceId: '', 31 | mtVersion: '', 32 | productIdList: '', 33 | shopId: '', 34 | shopMode: '', 35 | province: '', 36 | city: '', 37 | lat: '', 38 | lng: '', 39 | token: '', 40 | cookie: '', 41 | }) 42 | 43 | // 生成环境变量字符串,使用#作为分隔符 44 | function generateEnvString() { 45 | return [ 46 | state.phoneNumber, 47 | state.userId, 48 | state.deviceId, 49 | state.mtVersion, 50 | state.productIdList, 51 | `${state.shopId}^${state.shopMode}^${state.province}^${state.city}`, 52 | state.lat, 53 | state.lng, 54 | state.token, 55 | state.cookie, 56 | ].join('#') 57 | } 58 | 59 | return { 60 | state, 61 | generateEnvString, 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /src/types/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CommonHeader: typeof import('./../components/CommonHeader.vue')['default'] 11 | ElButton: typeof import('element-plus/es')['ElButton'] 12 | ElCard: typeof import('element-plus/es')['ElCard'] 13 | ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] 14 | ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] 15 | ElCollapse: typeof import('element-plus/es')['ElCollapse'] 16 | ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] 17 | ElDialog: typeof import('element-plus/es')['ElDialog'] 18 | ElForm: typeof import('element-plus/es')['ElForm'] 19 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 20 | ElIcon: typeof import('element-plus/es')['ElIcon'] 21 | ElInput: typeof import('element-plus/es')['ElInput'] 22 | ElOption: typeof import('element-plus/es')['ElOption'] 23 | ElRadio: typeof import('element-plus/es')['ElRadio'] 24 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] 25 | ElSelect: typeof import('element-plus/es')['ElSelect'] 26 | ElStep: typeof import('element-plus/es')['ElStep'] 27 | ElSteps: typeof import('element-plus/es')['ElSteps'] 28 | ElTag: typeof import('element-plus/es')['ElTag'] 29 | IMineGithub: typeof import('~icons/mine/github')['default'] 30 | RouterLink: typeof import('vue-router')['RouterLink'] 31 | RouterView: typeof import('vue-router')['RouterView'] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/axios/example/exampleInterceptorHooks.ts: -------------------------------------------------------------------------------- 1 | import type { ExpandAxiosResponse, InterceptorHooks } from '../types' 2 | 3 | /** 4 | * 📢 一般不用动此文件 5 | * 拦截器示例,按照项目实际情况编写 6 | */ 7 | 8 | export const exampleInterceptorHooks: InterceptorHooks = { 9 | requestInterceptor(config) { 10 | // 请求头部处理,如添加 token 11 | const token = 'token-value' 12 | if (token) { 13 | config!.headers!.Authorization = token 14 | } 15 | return config 16 | }, 17 | requestInterceptorCatch(err) { 18 | // 请求错误,这里可以用全局提示框进行提示 19 | return Promise.reject(err) 20 | }, 21 | 22 | responseInterceptor(result) { 23 | // 因为 axios 返回不支持扩展自定义配置,需要自己断言一下 24 | const res = result as ExpandAxiosResponse 25 | // 与后端约定的请求成功码 26 | const SUCCESS_CODE = 1 27 | if (res.status !== 200) 28 | return Promise.reject(res) 29 | if (res.data.code !== SUCCESS_CODE) { 30 | if (res.config.requestOptions?.globalErrorMessage) { 31 | // 这里全局提示错误 32 | console.error(res.data.message) 33 | } 34 | return Promise.reject(res.data) 35 | } 36 | if (res.config.requestOptions?.globalSuccessMessage) { 37 | // 这里全局提示请求成功 38 | console.log(res.data.message) 39 | } 40 | // 请求返回值,建议将 返回值 进行解构 41 | return res.data.result 42 | }, 43 | responseInterceptorCatch(err) { 44 | // 这里用来处理 http 常见错误,进行全局提示 45 | const mapErrorStatus = new Map([ 46 | [400, '请求方式错误'], 47 | [401, '请重新登录'], 48 | [403, '拒绝访问'], 49 | [404, '请求地址有误'], 50 | [500, '服务器出错'], 51 | ]) 52 | const message = mapErrorStatus.get(err.response.status) || '请求出错,请稍后再试' 53 | // 此处全局报错 54 | console.error(message) 55 | return Promise.reject(err.response) 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /functions/mtstaticapi/[[path]].js: -------------------------------------------------------------------------------- 1 | export async function onRequest(context) { 2 | const { request, env, params } = context 3 | const VITE_MT_SHOP_STATIC_URL = env.VITE_MT_SHOP_STATIC_URL // 从 Cloudflare Pages 环境变量读取 4 | 5 | if (!VITE_MT_SHOP_STATIC_URL) { 6 | return new Response('Target URL not configured for mtstaticapi proxy', { status: 500 }) 7 | } 8 | 9 | const subPathParts = params.path || [] 10 | const subPath = subPathParts.join('/') 11 | 12 | let queryString = '' 13 | const queryParamIndex = request.url.indexOf('?') 14 | if (queryParamIndex !== -1) { 15 | queryString = request.url.substring(queryParamIndex) 16 | } 17 | 18 | const targetUrl = `${VITE_MT_SHOP_STATIC_URL}/${subPath}${queryString}` 19 | 20 | const headers = new Headers(request.headers) 21 | 22 | try { 23 | const response = await fetch(targetUrl, { 24 | method: request.method, 25 | headers, 26 | body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, 27 | redirect: 'follow', 28 | }) 29 | 30 | const responseHeaders = new Headers() 31 | response.headers.forEach((value, key) => { 32 | if (!key.toLowerCase().startsWith('cf-') && key.toLowerCase() !== 'x-frame-options') { 33 | responseHeaders.append(key, value) 34 | } 35 | }) 36 | 37 | if (response.headers.has('content-type')) { 38 | responseHeaders.set('content-type', response.headers.get('content-type')) 39 | } 40 | 41 | return new Response(response.body, { 42 | status: response.status, 43 | statusText: response.statusText, 44 | headers: responseHeaders, 45 | }) 46 | } 47 | catch (error) { 48 | console.error(`Proxy error for /mtstaticapi/${subPath}:`, error) 49 | return new Response('Proxy error', { status: 502 }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/stores/modules/user.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { defineStore } from 'pinia' 3 | 4 | // 用户生成的历史环境变量 5 | export interface UserEnvHistory { 6 | date: string 7 | remark: string 8 | type: 'add' | 'update' 9 | uuid: string 10 | env: string 11 | } 12 | 13 | export const useUserStore = defineStore('user', () => { 14 | // 用户生成的历史环境变量 15 | const historyEnvList = ref([]) 16 | 17 | // 添加计算属性:按时间倒序排序的历史记录 18 | const sortedHistoryEnvList = computed(() => { 19 | return [...historyEnvList.value].sort((a, b) => { 20 | return new Date(b.date).getTime() - new Date(a.date).getTime() 21 | }) 22 | }) 23 | 24 | // 保存用户生成的环境变量 25 | function saveEnv(env: UserEnvHistory) { 26 | historyEnvList.value.push(env) 27 | } 28 | 29 | // 刷新cookie 30 | function refreshCookie() { 31 | return new Promise((resolve) => { 32 | // TODO 待实现 33 | resolve() 34 | }) 35 | } 36 | 37 | // 根据 uuid 获取历史记录 38 | function getHistoryByUuid(uuid: string) { 39 | return historyEnvList.value.find(item => item.uuid === uuid) 40 | } 41 | 42 | // 更新历史记录 43 | function updateEnv(uuid: string, env: Partial) { 44 | const index = historyEnvList.value.findIndex(item => item.uuid === uuid) 45 | if (index !== -1) { 46 | historyEnvList.value[index] = { 47 | ...historyEnvList.value[index], 48 | ...env, 49 | date: dayjs().format('YYYY-MM-DD HH:mm:ss'), // 更新时间 50 | } 51 | } 52 | } 53 | 54 | // 清空历史记录 55 | function clearHistory() { 56 | historyEnvList.value = [] 57 | } 58 | 59 | return { 60 | refreshCookie, 61 | saveEnv, 62 | historyEnvList, 63 | sortedHistoryEnvList, 64 | getHistoryByUuid, 65 | updateEnv, 66 | clearHistory, 67 | } 68 | }, { 69 | persist: { 70 | storage: localStorage, 71 | pick: ['historyEnvList'], 72 | }, 73 | }) 74 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | workflow_dispatch: # 手动触发 8 | 9 | env: 10 | REGISTRY: docker.io 11 | IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/imaotai-env-generator 12 | 13 | # 添加权限控制 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | jobs: 19 | build: 20 | # 添加权限检查 21 | if: github.actor == 'AkenClub' # 替换成你的 GitHub 用户名 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | # 设置 QEMU 以支持跨平台构建 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v3 31 | 32 | - name: Set up Docker Buildx 33 | # Buildx 进行多平台构建 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: Login to Docker Hub 37 | uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKER_USERNAME }} 40 | password: ${{ secrets.DOCKER_PASSWORD }} 41 | 42 | - name: Extract Docker metadata 43 | id: meta 44 | uses: docker/metadata-action@v5 45 | with: 46 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 47 | tags: | 48 | type=semver,pattern={{version}} 49 | type=semver,pattern={{major}}.{{minor}} 50 | type=raw,value=latest 51 | 52 | - name: Build and push Docker image 53 | uses: docker/build-push-action@v5 54 | with: 55 | context: . 56 | push: true 57 | # 修改:指定需要构建的平台 58 | platforms: linux/amd64,linux/arm64 # 你想要支持的平台列表 59 | tags: ${{ steps.meta.outputs.tags }} 60 | labels: ${{ steps.meta.outputs.labels }} 61 | # 缓存对多平台构建仍然有效 62 | cache-from: type=gha 63 | cache-to: type=gha,mode=max 64 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # 1. 前端构建阶段 (frontend-builder) 3 | # ============================================================================== 4 | FROM node:18-alpine AS frontend-builder 5 | WORKDIR /src 6 | 7 | # 启用 Corepack 以使用项目定义的 Yarn 版本 8 | RUN corepack enable 9 | 10 | # 复制所有项目文件到工作目录 11 | COPY . . 12 | 13 | # 使用 mount 来缓存前端依赖并进行安装 14 | # 移除了 --immutable-cache,允许在缓存未命中时从网络下载依赖 15 | RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \ 16 | yarn install --immutable 17 | 18 | # 运行构建命令 19 | RUN yarn build-only 20 | 21 | # ============================================================================== 22 | # 2. 服务端依赖阶段 (backend-deps) 23 | # ============================================================================== 24 | FROM node:18-alpine AS backend-deps 25 | WORKDIR /app/server 26 | 27 | # 启用 Corepack 28 | RUN corepack enable 29 | 30 | # 复制后端的 package.json 和 yarn.lock 文件 31 | # [重要建议]: 请在你的 server/ 目录下生成并提交 yarn.lock 文件 32 | COPY server/package.json server/yarn.lock* ./ 33 | 34 | # 只安装生产环境的服务端依赖 35 | RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \ 36 | yarn install --production --immutable 37 | 38 | # ============================================================================== 39 | # 3. 最终镜像阶段 40 | # ============================================================================== 41 | FROM node:18-alpine 42 | ENV NODE_ENV=production 43 | WORKDIR /app 44 | 45 | # 从服务端依赖阶段复制必要文件 46 | COPY --from=backend-deps /app/server/node_modules ./server/node_modules 47 | COPY --from=backend-deps /app/server/package.json ./server/package.json 48 | # 如果你为后端生成了 yarn.lock,也建议一并复制 49 | COPY --from=backend-deps /app/server/yarn.lock* ./server/ 50 | 51 | # 复制后端应用代码 52 | COPY server/app.js ./server/ 53 | 54 | # 从前端构建阶段复制构建产物 55 | COPY --from=frontend-builder /src/dist ./dist 56 | 57 | EXPOSE 12999 58 | CMD ["node", "server/app.js"] -------------------------------------------------------------------------------- /functions/mtappapi/[[path]].js: -------------------------------------------------------------------------------- 1 | export async function onRequest(context) { 2 | const { request, env, params } = context 3 | const VITE_MT_APP_API_URL = env.VITE_MT_APP_API_URL // 从 Cloudflare Pages 环境变量读取 4 | 5 | if (!VITE_MT_APP_API_URL) { 6 | return new Response('Target URL not configured for mtappapi proxy', { status: 500 }) 7 | } 8 | 9 | const subPathParts = params.path || [] 10 | const subPath = subPathParts.join('/') 11 | 12 | let queryString = '' 13 | const queryParamIndex = request.url.indexOf('?') 14 | if (queryParamIndex !== -1) { 15 | queryString = request.url.substring(queryParamIndex) 16 | } 17 | 18 | const targetUrl = `${VITE_MT_APP_API_URL}/${subPath}${queryString}` 19 | 20 | // 复制原始请求的 Headers,并修改 User-Agent 21 | const headers = new Headers(request.headers) 22 | headers.set('User-Agent', 'iOS;16.3;Apple;?unrecognized?') 23 | // 可根据需要删除或修改其他头部,例如 Host,但通常 Cloudflare 会正确处理 24 | // headers.delete('Host'); 25 | 26 | try { 27 | const response = await fetch(targetUrl, { 28 | method: request.method, 29 | headers, 30 | body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, 31 | redirect: 'follow', 32 | }) 33 | 34 | const responseHeaders = new Headers() 35 | response.headers.forEach((value, key) => { 36 | if (!key.toLowerCase().startsWith('cf-') && key.toLowerCase() !== 'x-frame-options') { 37 | responseHeaders.append(key, value) 38 | } 39 | }) 40 | 41 | if (response.headers.has('content-type')) { 42 | responseHeaders.set('content-type', response.headers.get('content-type')) 43 | } 44 | 45 | return new Response(response.body, { 46 | status: response.status, 47 | statusText: response.statusText, 48 | headers: responseHeaders, 49 | }) 50 | } 51 | catch (error) { 52 | console.error(`Proxy error for /mtappapi/${subPath}:`, error) 53 | return new Response('Proxy error', { status: 502 }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import process from 'node:process' 3 | import { fileURLToPath } from 'node:url' 4 | import express from 'express' 5 | import { createProxyMiddleware } from 'http-proxy-middleware' 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 8 | const distPath = path.join(__dirname, '../dist') 9 | 10 | const app = express() 11 | 12 | // 添加静态文件服务 13 | app.use(express.static(distPath)) 14 | 15 | // 环境变量 16 | const env = { 17 | VITE_APP_STORE_API_PREFIX: '/appleapi', 18 | VITE_APP_STORE_URL: 'https://apps.apple.com', 19 | VITE_MT_SHOP_STATIC_API_PREFIX: '/mtstaticapi', 20 | VITE_MT_SHOP_STATIC_URL: 'https://static.moutai519.com.cn', 21 | VITE_MT_APP_API_PREFIX: '/mtappapi', 22 | VITE_MT_APP_API_URL: 'https://app.moutai519.com.cn', 23 | } 24 | 25 | // 配置代理 26 | app.use( 27 | env.VITE_APP_STORE_API_PREFIX, 28 | createProxyMiddleware({ 29 | target: env.VITE_APP_STORE_URL, 30 | changeOrigin: true, 31 | pathRewrite: path => 32 | path.replace(new RegExp(`^${env.VITE_APP_STORE_API_PREFIX}`), ''), 33 | }), 34 | ) 35 | 36 | app.use( 37 | env.VITE_MT_SHOP_STATIC_API_PREFIX, 38 | createProxyMiddleware({ 39 | target: env.VITE_MT_SHOP_STATIC_URL, 40 | changeOrigin: true, 41 | pathRewrite: path => 42 | path.replace(new RegExp(`^${env.VITE_MT_SHOP_STATIC_API_PREFIX}`), ''), 43 | }), 44 | ) 45 | 46 | app.use( 47 | env.VITE_MT_APP_API_PREFIX, 48 | createProxyMiddleware({ 49 | target: env.VITE_MT_APP_API_URL, 50 | changeOrigin: true, 51 | headers: { 52 | // 浏览器控制台中出现 Refused to set unsafe header "User-Agent" 报错是浏览器的安全策略导致, 53 | // 前端 axios 虽然设置了 UA,但是基于这个安全策略的原因,浏览器会自动修改 UA 为浏览器的 UA。 54 | // 所以需要在这里重新设置 UA,避免在茅台 APP 接口中出现浏览器的 UA。 55 | 'User-Agent': 'iOS;16.3;Apple;?unrecognized?', 56 | }, 57 | pathRewrite: path => 58 | path.replace(new RegExp(`^${env.VITE_MT_APP_API_PREFIX}`), ''), 59 | }), 60 | ) 61 | 62 | // 处理 SPA 路由,所有未匹配的请求都返回 index.html 63 | app.get('*', (_, res) => { 64 | res.sendFile(path.join(distPath, 'index.html')) 65 | }) 66 | 67 | // 启动服务器 68 | const PORT = process.env.PORT || 12999 69 | app.listen(PORT, () => { 70 | console.log(`Server is running on http://localhost:${PORT}`) 71 | }) 72 | -------------------------------------------------------------------------------- /src/components/CommonHeader.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 79 | -------------------------------------------------------------------------------- /functions/appleapi/[[path]].js: -------------------------------------------------------------------------------- 1 | export async function onRequest(context) { 2 | const { request, env, params } = context 3 | const VITE_APP_STORE_URL = env.VITE_APP_STORE_URL // 从 Cloudflare Pages 环境变量读取 4 | 5 | if (!VITE_APP_STORE_URL) { 6 | return new Response('Target URL not configured for appleapi proxy', { status: 500 }) 7 | } 8 | 9 | // 获取原始请求路径中 [[path]] 匹配到的部分 10 | const subPathParts = params.path || [] 11 | const subPath = subPathParts.join('/') 12 | 13 | // 构建目标 URL 14 | let queryString = '' 15 | const queryParamIndex = request.url.indexOf('?') 16 | if (queryParamIndex !== -1) { 17 | queryString = request.url.substring(queryParamIndex) 18 | } 19 | 20 | const targetUrl = `${VITE_APP_STORE_URL}/${subPath}${queryString}` 21 | 22 | // 复制原始请求的 Headers 23 | const headers = new Headers(request.headers) 24 | // Cloudflare 会自动处理 Host header,通常不需要手动设置 25 | // headers.set('Host', new URL(VITE_APP_STORE_URL).host); 26 | 27 | try { 28 | const response = await fetch(targetUrl, { 29 | method: request.method, 30 | headers, 31 | body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, 32 | redirect: 'follow', 33 | }) 34 | 35 | // 直接返回从目标服务器获取的响应 36 | // 注意:直接传递 response.headers 可能会包含 Cloudflare 不允许客户端设置的头部 (e.g. cf-ray) 37 | // 最好是创建一个新的 Headers 对象,只复制必要的头部 38 | const responseHeaders = new Headers() 39 | response.headers.forEach((value, key) => { 40 | // 过滤掉一些不应直接传递的头部,或者根据需要进行选择 41 | // 例如: 'content-encoding', 'transfer-encoding' 通常由 Cloudflare 处理 42 | if (!key.toLowerCase().startsWith('cf-') && key.toLowerCase() !== 'x-frame-options') { 43 | responseHeaders.append(key, value) 44 | } 45 | }) 46 | 47 | // 确保内容类型正确 48 | if (response.headers.has('content-type')) { 49 | responseHeaders.set('content-type', response.headers.get('content-type')) 50 | } 51 | 52 | return new Response(response.body, { 53 | status: response.status, 54 | statusText: response.statusText, 55 | headers: responseHeaders, 56 | }) 57 | } 58 | catch (error) { 59 | console.error(`Proxy error for /appleapi/${subPath}:`, error) 60 | return new Response('Proxy error', { status: 502 }) // 502 Bad Gateway 更适合代理错误 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/style/base.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | /* color palette from */ 3 | --vt-c-white: #fff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | --vt-c-black: #181818; 7 | --vt-c-black-soft: #222; 8 | --vt-c-black-mute: #282828; 9 | --vt-c-indigo: #2c3e50; 10 | --vt-c-divider-light-1: rgb(60 60 60 / 29%); 11 | --vt-c-divider-light-2: rgb(60 60 60 / 12%); 12 | --vt-c-divider-dark-1: rgb(84 84 84 / 65%); 13 | --vt-c-divider-dark-2: rgb(84 84 84 / 48%); 14 | --vt-c-text-light-1: var(--vt-c-indigo); 15 | --vt-c-text-light-2: rgb(60 60 60 / 66%); 16 | --vt-c-text-dark-1: var(--vt-c-white); 17 | --vt-c-text-dark-2: rgb(235 235 235 / 64%); 18 | 19 | /* semantic color variables for this project */ 20 | --color-background: var(--vt-c-white); 21 | --color-background-soft: var(--vt-c-white-soft); 22 | --color-background-mute: var(--vt-c-white-mute); 23 | --color-border: var(--vt-c-divider-light-2); 24 | --color-border-hover: var(--vt-c-divider-light-1); 25 | --color-heading: var(--vt-c-text-light-1); 26 | --color-text: var(--vt-c-text-light-1); 27 | --section-gap: 160px; 28 | } 29 | 30 | @media (prefers-color-scheme: dark) { 31 | :root { 32 | --color-background: var(--vt-c-black); 33 | --color-background-soft: var(--vt-c-black-soft); 34 | --color-background-mute: var(--vt-c-black-mute); 35 | --color-border: var(--vt-c-divider-dark-2); 36 | --color-border-hover: var(--vt-c-divider-dark-1); 37 | --color-heading: var(--vt-c-text-dark-1); 38 | --color-text: var(--vt-c-text-dark-2); 39 | } 40 | } 41 | 42 | *, 43 | *::before, 44 | *::after { 45 | position: relative; 46 | box-sizing: border-box; 47 | margin: 0; 48 | font-weight: normal; 49 | } 50 | 51 | body { 52 | font-family: 53 | Inter, 54 | -apple-system, 55 | BlinkMacSystemFont, 56 | 'Segoe UI', 57 | Roboto, 58 | Oxygen, 59 | Ubuntu, 60 | Cantarell, 61 | 'Fira Sans', 62 | 'Droid Sans', 63 | 'Helvetica Neue', 64 | sans-serif; 65 | font-size: 15px; 66 | line-height: 1.6; 67 | color: var(--color-text); 68 | background: var(--color-background-mute); 69 | transition: 70 | color 0.5s, 71 | background-color 0.5s; 72 | text-rendering: optimizelegibility; 73 | -webkit-font-smoothing: antialiased; 74 | -moz-osx-font-smoothing: grayscale; 75 | } 76 | -------------------------------------------------------------------------------- /src/views/generate/components/StepTwo.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 84 | -------------------------------------------------------------------------------- /src/utils/axios/AxiosRequest.ts: -------------------------------------------------------------------------------- 1 | import type { ExpandAxiosRequestConfig, InterceptorHooks } from './types' 2 | import axios, { type AxiosInstance, type AxiosResponse, type CustomParamsSerializer } from 'axios' 3 | import { stringify } from 'qs' 4 | 5 | /** 6 | * 📢 一般不用动此文件,使用时直接 new Request 即可 7 | */ 8 | 9 | // 导出Request类,可以用来自定义传递配置来创建实例 10 | export default class AxiosRequest { 11 | // axios 实例 12 | private _instance: AxiosInstance 13 | 14 | // 默认配置 15 | private _defaultConfig: ExpandAxiosRequestConfig = { 16 | // 默认请求超时时间 17 | timeout: 10000, 18 | // 数组格式参数序列化(https://github.com/axios/axios/issues/5142) 19 | paramsSerializer: { 20 | serialize: stringify as unknown as CustomParamsSerializer, 21 | }, 22 | } 23 | 24 | private _interceptorHooks?: InterceptorHooks 25 | 26 | constructor(config: ExpandAxiosRequestConfig) { 27 | // 使用axios.create创建axios实例 28 | this._instance = axios.create(Object.assign(this._defaultConfig, config)) 29 | this._interceptorHooks = config.interceptorHooks 30 | this.setupInterceptors() 31 | } 32 | 33 | // 通用拦截,在初始化时就进行注册和运行,对基础属性进行处理 34 | private setupInterceptors() { 35 | this._instance.interceptors.request.use( 36 | this._interceptorHooks?.requestInterceptor, 37 | this._interceptorHooks?.requestInterceptorCatch, 38 | ) 39 | this._instance.interceptors.response.use( 40 | this._interceptorHooks?.responseInterceptor, 41 | this._interceptorHooks?.responseInterceptorCatch, 42 | ) 43 | } 44 | 45 | // 定义核心请求 46 | public request(config: ExpandAxiosRequestConfig): Promise { 47 | // !!!⚠️ 注意:axios 已经将请求使用 promise 封装过了 48 | // 这里直接返回,不需要我们再使用 promise 封装一层 49 | return this._instance.request(config) 50 | } 51 | 52 | public get(url: string, config?: ExpandAxiosRequestConfig): Promise { 53 | return this._instance.get(url, config) 54 | } 55 | 56 | public post(url: string, data?: any, config?: ExpandAxiosRequestConfig): Promise { 57 | return this._instance.post(url, data, config) 58 | } 59 | 60 | public put(url: string, data?: any, config?: ExpandAxiosRequestConfig): Promise { 61 | return this._instance.put(url, data, config) 62 | } 63 | 64 | public delete(url: string, config?: ExpandAxiosRequestConfig): Promise { 65 | return this._instance.delete(url, config) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imaotai-env-generator", 3 | "type": "module", 4 | "version": "1.2.4", 5 | "private": true, 6 | "packageManager": "yarn@4.9.1", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "run-p type-check build-only", 10 | "build-only": "vite build", 11 | "build:analyze": "cross-env VITE_ANALYZE=true vite build", 12 | "preview": "vite preview", 13 | "type-check": "vue-tsc --noEmit --skipLibCheck", 14 | "lint:lint-staged": "lint-staged -c lint-staged.config.js", 15 | "lint:eslint": "eslint --fix", 16 | "lint:stylelint": "stylelint \"**/*.{vue,css,scss,postcss,less,html}\" --fix", 17 | "prepare": "husky install" 18 | }, 19 | "dependencies": { 20 | "axios": "^1.7.9", 21 | "element-plus": "^2.9.2", 22 | "md5": "^2.3.0", 23 | "nprogress": "^0.2.0", 24 | "pinia": "^2.3.0", 25 | "pinia-plugin-persistedstate": "^4.2.0", 26 | "qs": "^6.13.1", 27 | "uuid": "^11.0.5", 28 | "vue": "^3.5.13", 29 | "vue-router": "^4.5.0" 30 | }, 31 | "devDependencies": { 32 | "@antfu/eslint-config": "^3.12.1", 33 | "@commitlint/cli": "^19.6.1", 34 | "@commitlint/config-conventional": "^19.6.0", 35 | "@iconify-json/ep": "^1.2.2", 36 | "@tsconfig/node22": "^22.0.0", 37 | "@types/md5": "^2.3.5", 38 | "@types/node": "22", 39 | "@types/nprogress": "^0.2.3", 40 | "@types/qs": "^6.9.17", 41 | "@types/uuid": "^10.0.0", 42 | "@vitejs/plugin-vue": "^5.2.1", 43 | "@vue/compiler-sfc": "^3.5.13", 44 | "@vue/tsconfig": "^0.7.0", 45 | "autoprefixer": "^10.4.20", 46 | "cross-env": "^7.0.3", 47 | "eslint": "^9.17.0", 48 | "eslint-plugin-format": "^0.1.3", 49 | "husky": "^9.1.7", 50 | "lint-staged": "^15.3.0", 51 | "npm-run-all": "^4.1.5", 52 | "postcss-html": "^1.7.0", 53 | "prettier": "^3.4.2", 54 | "rollup-plugin-visualizer": "^5.14.0", 55 | "sass": "^1.83.1", 56 | "stylelint": "^16.12.0", 57 | "stylelint-config-recess-order": "^5.1.1", 58 | "stylelint-config-standard": "^36.0.1", 59 | "stylelint-config-standard-scss": "^14.0.0", 60 | "stylelint-config-standard-vue": "^1.0.0", 61 | "stylelint-prettier": "^5.0.2", 62 | "stylelint-scss": "^6.10.0", 63 | "tailwindcss": "^3.4.17", 64 | "typescript": "^5.7.2", 65 | "unplugin-auto-import": "^0.19.0", 66 | "unplugin-icons": "^0.22.0", 67 | "unplugin-vue-components": "^0.28.0", 68 | "vite": "^6.0.7", 69 | "vue-tsc": "^2.2.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/api/imaotai/user.ts: -------------------------------------------------------------------------------- 1 | import { axiosRequest } from '@/api/http' 2 | import md5 from 'md5' 3 | 4 | interface SendCodeResponse { 5 | code: number 6 | message?: string 7 | } 8 | 9 | interface LoginResponse { 10 | code: number 11 | message?: string 12 | data: { 13 | userId: string 14 | token: string 15 | cookie: string 16 | [key: string]: any 17 | } 18 | } 19 | 20 | const MT_APP_API_PREFIX = import.meta.env.VITE_MT_APP_API_PREFIX 21 | 22 | /** 23 | * 发送验证码 24 | */ 25 | export async function sendVerifyCode(mobile: string, deviceId: string, mtVersion: string): Promise { 26 | const timestamp = Date.now() 27 | const data = { 28 | mobile, 29 | md5: generateSignature(mobile, timestamp), 30 | timestamp: String(timestamp), 31 | } 32 | 33 | const headers = { 34 | 'MT-Device-ID': deviceId, 35 | 'MT-APP-Version': mtVersion, 36 | 'User-Agent': 'iOS;16.3;Apple;?unrecognized?', 37 | 'Content-Type': 'application/json', 38 | } 39 | 40 | await axiosRequest.post( 41 | `${MT_APP_API_PREFIX}/xhr/front/user/register/vcode`, 42 | data, 43 | { headers }, 44 | ) 45 | } 46 | 47 | /** 48 | * 登录 49 | */ 50 | export async function login(params: { 51 | mobile: string 52 | vCode: string 53 | deviceId: string 54 | mtVersion: string 55 | }): Promise { 56 | const timestamp = Date.now() 57 | const data = { 58 | 'mobile': params.mobile, 59 | 'vCode': params.vCode, 60 | 'md5': generateSignature(params.mobile + params.vCode, timestamp), 61 | 'timestamp': String(timestamp), 62 | 'MT-APP-Version': params.mtVersion, 63 | } 64 | 65 | const headers = { 66 | 'MT-Device-ID': params.deviceId, 67 | 'MT-APP-Version': params.mtVersion, 68 | 'User-Agent': 'iOS;16.3;Apple;?unrecognized?', 69 | 'Content-Type': 'application/json', 70 | } 71 | 72 | const response = await axiosRequest.post( 73 | `${MT_APP_API_PREFIX}/xhr/front/user/register/login`, 74 | data, 75 | { headers, requestOptions: { globalErrorMessage: false } }, 76 | ) 77 | 78 | if (!response || response.code !== 2000 || !response.data) { 79 | throw new Error(response.message || '登录失败') 80 | } 81 | 82 | return response.data 83 | } 84 | 85 | // 生成签名 86 | function generateSignature(content: string, timestamp: number): string { 87 | const SALT = '2af72f100c356273d46284f6fd1dfc08' 88 | const text = SALT + content + String(timestamp) 89 | return md5(text) 90 | } 91 | -------------------------------------------------------------------------------- /src/api/imaotai/shop.ts: -------------------------------------------------------------------------------- 1 | import { axiosRequest } from '@/api/http' 2 | 3 | // 商品信息接口类型定义 4 | interface ProductInfo { 5 | itemCode: string 6 | title: string 7 | } 8 | 9 | interface ProductResponse { 10 | code: number 11 | data: { 12 | sessionId: string 13 | itemList: Array<{ 14 | itemCode: string 15 | title: string 16 | pictureV2?: string 17 | }> 18 | } 19 | } 20 | 21 | // 商店信息接口类型定义 22 | interface ShopInfo { 23 | lat: string 24 | lng: string 25 | name: string 26 | shopId: string 27 | fullAddress: string 28 | cityName: string 29 | provinceName: string 30 | } 31 | 32 | interface ShopResourceResponse { 33 | code: number 34 | data: { 35 | myserviceshops: { 36 | url: string 37 | } 38 | } 39 | } 40 | 41 | interface ShopsData { 42 | [key: string]: ShopInfo 43 | } 44 | 45 | const MT_SHOP_STATIC_API_PREFIX = import.meta.env.VITE_MT_SHOP_STATIC_API_PREFIX 46 | 47 | /** 48 | * 获取可预约的商品信息 49 | */ 50 | export async function getProductInfo() { 51 | try { 52 | const nowDate = Number.parseInt((new Date().getTime() / 1000).toString()) 53 | const timestamp = (nowDate - (nowDate % 86400) - 3600 * 8) * 1000 54 | const response = await axiosRequest.get( 55 | `${MT_SHOP_STATIC_API_PREFIX}/mt-backend/xhr/front/mall/index/session/get/${timestamp}`, 56 | ) 57 | 58 | if (response.code !== 2000) { 59 | throw new Error('获取商品信息失败') 60 | } 61 | 62 | const { sessionId, itemList } = response.data 63 | const formattedItemList = itemList.map(item => ({ 64 | itemCode: item.itemCode, 65 | title: item.title || `未知商品,可结合该商品图片链接来判断:${item.pictureV2 || '无图片信息'}`, 66 | })) 67 | 68 | return { 69 | sessionId, 70 | itemList: formattedItemList, 71 | } 72 | } 73 | catch (error: any) { 74 | throw new Error(`获取商品信息失败:${error?.message || '未知错误'}`) 75 | } 76 | } 77 | 78 | /** 79 | * 获取售卖商店信息 80 | */ 81 | export async function getShopInfo(provinceName: string, cityName: string): Promise { 82 | try { 83 | const resourceResponse = await axiosRequest.get( 84 | `${MT_SHOP_STATIC_API_PREFIX}/mt-backend/xhr/front/mall/resource/get`, 85 | ) 86 | 87 | if (resourceResponse.code !== 2000) { 88 | throw new Error('获取资源信息失败 ') 89 | } 90 | 91 | const myserviceshopsUrl = resourceResponse.data.myserviceshops.url 92 | const shopsResponse = await axiosRequest.get(myserviceshopsUrl) 93 | 94 | const result: ShopInfo[] = [] 95 | Object.values(shopsResponse).forEach((shop) => { 96 | if (shop.provinceName === provinceName && shop.cityName === cityName) { 97 | result.push({ 98 | lat: shop.lat, 99 | lng: shop.lng, 100 | name: shop.name, 101 | shopId: shop.shopId, 102 | fullAddress: shop.fullAddress, 103 | cityName: shop.cityName, 104 | provinceName: shop.provinceName, 105 | }) 106 | } 107 | }) 108 | 109 | return result 110 | } 111 | catch (error: any) { 112 | throw new Error(`获取商店信息失败:${error?.message || '未知错误'}`) 113 | } 114 | } 115 | 116 | export type { ProductInfo, ShopInfo } 117 | -------------------------------------------------------------------------------- /src/api/http/customInterceptorHooks.ts: -------------------------------------------------------------------------------- 1 | import type { ExpandAxiosResponse, InterceptorHooks } from '@/utils/axios/types' 2 | import { axiosRequest } from '@/api/http/index' 3 | import { useUserStore } from '@/stores/modules/user' 4 | import NProgress from '@/utils/progress' 5 | 6 | // 与后端约定的请求成功码 7 | const SUCCESS_CODE = 2000 8 | 9 | export function customInterceptorHooks(): InterceptorHooks { 10 | return { 11 | requestInterceptor(config) { 12 | console.log('请求拦截:', config.data?.type, config) 13 | 14 | // 开启进度条动画 15 | NProgress.start() 16 | return config 17 | }, 18 | requestInterceptorCatch(err) { 19 | console.log('请求拦截 异常:', err) 20 | // 请求错误,这里可以用全局提示框进行提示 21 | return Promise.reject(err) 22 | }, 23 | 24 | responseInterceptor(response) { 25 | // 关闭进度条动画 26 | NProgress.done() 27 | 28 | // 因为 axios 返回不支持扩展自定义配置,需要自己断言一下 29 | const res = response as ExpandAxiosResponse 30 | console.log('响应拦截:', res.config.data, res) 31 | 32 | if (res.config.requestOptions?.skipResponseInterceptor) { 33 | return res.data 34 | } 35 | 36 | if (res.data.code && res.data.code === 403) { 37 | if (!axiosRequest.isRefreshingToken) { 38 | axiosRequest.isRefreshingToken = true 39 | useUserStore() 40 | .refreshCookie() 41 | .then(() => { 42 | console.log('刷新 cookie') 43 | // 执行缓存的请求 44 | axiosRequest.executeCachedRequests() 45 | }) 46 | .finally(() => { 47 | axiosRequest.isRefreshingToken = false 48 | }) 49 | } 50 | // 将原来请求保存,等待刷新 cookie 后回调重新发起请求再返回结果 51 | return axiosRequest.retryOriginalRequest(response.config) 52 | } 53 | 54 | if (res.status !== 200) 55 | return Promise.reject(res) 56 | 57 | // 在下载文件时,code 不存在,但是应该直接放行,让其完成下载 58 | if (res.data.code && res.data.code !== SUCCESS_CODE) { 59 | // 所以这里只判断存在 code 且不对应设置的成功码 的时候 60 | if (res.config.requestOptions?.globalErrorMessage) { 61 | // 这里全局提示错误 62 | console.error('后台返回的状态码 非200:', res.data.code, res.data.message) 63 | res.config.requestOptions?.globalErrorMessageHandle && res.config.requestOptions?.globalErrorMessageHandle(res.data.message || '未知错误') 64 | } 65 | return Promise.reject(res.data) 66 | } 67 | if (res.config.requestOptions?.globalSuccessMessage) { 68 | // 这里全局提示请求成功 69 | console.log('全局提示请求成功', res.data) 70 | } 71 | // 请求返回值,建议将 返回值 进行解构 72 | return res.data 73 | }, 74 | responseInterceptorCatch(err) { 75 | console.log('响应拦截 异常:', err) 76 | 77 | // 关闭进度条动画 78 | NProgress.done() 79 | 80 | // 统一处理所有错误码 81 | const mapErrorStatus = new Map([ 82 | [400, '请求方式错误'], 83 | [401, '请重新登录'], 84 | [403, '拒绝访问'], 85 | [404, '请求地址有误'], 86 | [500, '服务器出错'], 87 | [482, '请更新应用版本号后重试'], 88 | ]) 89 | const message = (err.response && mapErrorStatus.get(err.response.status)) || err.response?.data?.message || '请求出错,请稍后再试' 90 | if (err.config.requestOptions?.globalErrorMessage) { 91 | // 这里全局提示错误 92 | console.error('响应异常:', message) 93 | err.config.requestOptions?.globalErrorMessageHandle 94 | && err.config.requestOptions?.globalErrorMessageHandle(message) 95 | } 96 | return Promise.reject(err.response.data) 97 | }, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { accessSync, readdirSync } from 'node:fs' 2 | import path from 'node:path' 3 | import node from 'node:process' 4 | import vue from '@vitejs/plugin-vue' 5 | import { visualizer } from 'rollup-plugin-visualizer' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 8 | import IconsResolver from 'unplugin-icons/resolver' 9 | import Icons from 'unplugin-icons/vite' 10 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 11 | import Components from 'unplugin-vue-components/vite' 12 | import { defineConfig, loadEnv } from 'vite' 13 | 14 | const pathSrc = path.resolve(__dirname, 'src') 15 | 16 | // https://vitejs.dev/config/ 17 | export default defineConfig(({ mode }) => { 18 | const env = loadEnv(mode, node.cwd()) 19 | console.log('运行环境:', env) 20 | const isProdMode = mode === 'production' 21 | 22 | // 预加载 element-plus 组件样式,避免开发环境重复加载 23 | const optimizeDepsIncludes: string[] = [] 24 | if (!isProdMode) { 25 | // 预加载element-plus 26 | readdirSync('node_modules/element-plus/es/components').forEach((dirname) => { 27 | try { 28 | // 同步检查文件是否存在,如果存在则添加到优化列表。这里不能使用异步,否则不会生效 29 | accessSync(`node_modules/element-plus/es/components/${dirname}/style/css.mjs`) 30 | optimizeDepsIncludes.push(`element-plus/es/components/${dirname}/style/css`) 31 | optimizeDepsIncludes.push(`element-plus/es/components/${dirname}/style/index`) 32 | } 33 | catch { 34 | // 文件不存在,跳过 35 | } 36 | }) 37 | } 38 | 39 | return { 40 | optimizeDeps: { 41 | include: optimizeDepsIncludes, 42 | }, 43 | 44 | server: { 45 | port: 12999, 46 | open: false, 47 | proxy: { 48 | [env.VITE_APP_STORE_API_PREFIX]: { 49 | target: env.VITE_APP_STORE_URL, 50 | changeOrigin: true, 51 | rewrite: path => path.replace(new RegExp(`^${env.VITE_APP_STORE_API_PREFIX}`), ''), 52 | }, 53 | [env.VITE_MT_SHOP_STATIC_API_PREFIX]: { 54 | target: env.VITE_MT_SHOP_STATIC_URL, 55 | changeOrigin: true, 56 | rewrite: path => path.replace(new RegExp(`^${env.VITE_MT_SHOP_STATIC_API_PREFIX}`), ''), 57 | }, 58 | [env.VITE_MT_APP_API_PREFIX]: { 59 | target: env.VITE_MT_APP_API_URL, 60 | changeOrigin: true, 61 | rewrite: path => path.replace(new RegExp(`^${env.VITE_MT_APP_API_PREFIX}`), ''), 62 | }, 63 | }, 64 | }, 65 | 66 | resolve: { 67 | alias: { 68 | '@': pathSrc, 69 | }, 70 | }, 71 | 72 | // vite 默认使用 esbuild。如果换成使用 terser 需要另外安装依赖。 73 | esbuild: { 74 | // 生产环境删除 console 和 debugger 75 | drop: isProdMode ? ['console', 'debugger'] : [], 76 | }, 77 | 78 | plugins: [ 79 | vue(), 80 | // 只在 VITE_ANALYZE 环境变量为 true 时才启用 visualizer 81 | env.VITE_ANALYZE === 'true' && visualizer({ 82 | open: true, 83 | filename: 'dist/stats.html', 84 | gzipSize: true, 85 | brotliSize: true, 86 | }), 87 | // 自动导入 API,例如在 script 中就不用导入 ref,就能直接使用 88 | AutoImport({ 89 | include: [ 90 | /\.[jt]sx?$/, // .ts, .tsx, .js, .jsx 91 | /\.vue$/, 92 | /\.vue\?vue/, // .vue 93 | /\.md$/, // .md 94 | ], 95 | imports: ['vue', 'vue-router'], 96 | resolvers: [ 97 | // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式) 98 | ElementPlusResolver(), 99 | ], 100 | // 用于 TS 101 | dts: path.resolve(pathSrc, 'types', 'auto-imports.d.ts'), 102 | vueTemplate: true, 103 | }), 104 | // 自动导入组件,可以在模板中直接写组件,不用 import 105 | Components({ 106 | resolvers: [ 107 | // 自动导入 Element Plus 组件 108 | ElementPlusResolver(), 109 | // 自动注册图标组件 110 | IconsResolver( 111 | // https://github.com/antfu/unplugin-icons#auto-importing 112 | { 113 | // {prefix}-{collection}-{icon} 114 | // 默认前缀为 i,在 Element 图标前面加 IEp 加图标名 即可自动导入,例如 IEpStar 115 | // prefix: 'icon', // false 表示不用前缀 116 | // 自动添加 @iconify-json/ep 包,ep 代表 ElementPlus 117 | // https://element-plus.org/zh-CN/component/icon.html 118 | enabledCollections: ['ep'], 119 | // 自动导入自定义的图标集合,默认前缀下,为 IMine 加图标文件名,例如 IMineArrow 120 | customCollections: ['mine'], 121 | }, 122 | ), 123 | ], 124 | dts: path.resolve(pathSrc, 'types', 'components.d.ts'), 125 | }), 126 | Icons({ 127 | autoInstall: true, 128 | // vue/compiler-sfc 包对 vue3 支持 129 | compiler: 'vue3', 130 | customCollections: { 131 | // 设置自定义的 SVG 目录,可以自动生成组件,可以配合上面自动导入 132 | mine: FileSystemIconLoader('./src/assets/svg', (svg) => { 133 | if (!svg) { 134 | console.error('Invalid SVG content') 135 | return '' // 返回一个空的 SVG 作为兜底 136 | } 137 | 138 | try { 139 | svg = svg 140 | // svg 文件最好去除宽高,框架会自动处理 141 | // 去除 svg 已经存在的 fill 和 stroke 142 | .replace(/\s*(width|height|fill|stroke)=".*?"/g, '') 143 | // 添加 fill="currentColor",让 svg 颜色跟随传入的颜色 144 | .replace(/^' // 返回一个空的 SVG 作为兜底 150 | } 151 | }), 152 | }, 153 | }), 154 | ], 155 | } 156 | }) 157 | -------------------------------------------------------------------------------- /src/views/generate/index.vue: -------------------------------------------------------------------------------- 1 | 129 | 130 | 212 | 213 | 238 | -------------------------------------------------------------------------------- /src/views/generate/components/StepFinal.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 223 | 224 | 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iMoutaiEnvGenerator 2 | 3 | ## 项目描述 4 | 5 | iMoutaiEnvGenerator 是一个专为 [ken-iMoutai-Script](https://github.com/AkenClub/ken-iMoutai-Script) 项目设计的配套前端工具。其主要目的是帮助用户快速生成脚本运行所需的青龙面板环境变量 `KEN_IMAOTAI_ENV`,大大简化了变量配置过程,提升用户体验。 6 | 7 | 用户只需根据提示依次填写手机号、验证码等必要信息,工具将实时生成对应的环境变量,并提供一键复制功能,方便用户直接粘贴到青龙面板。 8 | 9 | ## 功能特点 10 | 11 | - **表单输入**:简单易用的表单界面,用户只需输入或选择必要信息。 12 | - **实时生成**:根据用户输入动态生成 `KEN_IMAOTAI_ENV` 环境变量字符串。 13 | - **一键复制**:支持一键复制生成的环境变量到剪贴板。 14 | - **用户友好**:提供清晰的使用步骤和脚本说明。 15 | - **历史记录**:支持保存和更新历史环境变量记录。 16 | 17 | ## 使用截图 18 | 19 | ![首页](./src/assets/md/img_home.png) 20 | 21 |
22 | 23 | 更多截图 24 | 25 | ![新环境变量生成](./src/assets/md/img_generate-step-1.png) 26 | ![新环境变量生成](./src/assets/md/img_generate-step-final.png) 27 | ![更新旧环境变量](./src/assets/md/img_update.png) 28 | 29 |
30 | 31 | ## 推荐 Docker 一行命令部署 32 | 33 | 部署成功后,访问 `http://localhost:12999` 即可使用。 34 | 35 | ``` 36 | docker run -d --name imoutai-env-generator -p 12999:12999 ppken/imaotai-env-generator 37 | ``` 38 | 39 | ## 本地使用方法 40 | 41 | 1. **拉取代码并启动项目**: 42 | 43 | ``` 44 | git clone https://github.com/AkenClub/iMoutaiEnvGenerator.git 45 | cd iMoutaiEnvGenerator 46 | yarn install 47 | yarn dev 48 | ``` 49 | 50 | 2. **填写表单**: 51 | 52 | - 打开浏览器,访问 `http://localhost:12999`。 53 | - 根据表单提示输入信息。 54 | 55 | 3. **生成环境变量**: 56 | 57 | - 确认填写无误后,点击 "生成环境变量" 按钮。 58 | - 复制生成的 `KEN_IMAOTAI_ENV`。 59 | 60 | 4. **粘贴到青龙面板**: 61 | 62 | - 登录青龙面板,进入环境变量配置页面。 63 | - 新增变量 `KEN_IMAOTAI_ENV`,将生成的字符串粘贴进去,保存即可。 64 | 65 | ## Docker 构建使用方法 66 | 67 | 直接集成了前后端,直接启动即可。前端地址为 `http://localhost:12999`。 68 | 69 | ``` 70 | 71 | docker build -t imaotai-env-generator . 72 | 73 | docker run -d --name imaotai-env-generator -p 12999:12999 imaotai-env-generator 74 | ``` 75 | 76 | ## 注意事项 77 | 78 | 1. **关于 User-Agent 设置问题** 79 | - 现象:浏览器控制台可能出现 `Refused to set unsafe header "User-Agent"` 警告。 80 | - 原因:这是由浏览器的安全策略导致的,前端无法直接修改某些保护的请求头,前端的 axios 配置的 User-Agent 会被浏览器忽略。 81 | - 解决方案: 82 | - 推荐使用上述 Docker 构建。 83 | - 也可自行使用 node 启动项目,入口文件为 `server/app.js`。先执行打包命令 `yarn build`,生成 `dist` 目录,然后执行 `node server/app.js` 启动项目。 84 | - 此警告不会影响程序正常运行,可以忽略,但是为了避免在茅台 APP 接口中出现浏览器的 UA,建议在 `server/app.js` 中设置 User-Agent。 85 | 86 | ## CloudFlare 部署方式 87 | 88 |
89 | 90 | 点击展开 91 | 92 | 93 | 本项目可以方便地部署到 Cloudflare Pages,利用其全球 CDN 和 Serverless Functions (用于 API 代理)。 94 | 95 | ### 部署步骤 96 | 97 | 1. **代码准备**: 98 | 99 | - 确保您的代码已推送到 GitHub 或 GitLab 仓库。 100 | - 项目中已包含 `functions` 目录,其中包含用于 API 代理的 Cloudflare Pages Functions (`functions/appleapi/[[path]].js`, `functions/mtstaticapi/[[path]].js`, `functions/mtappapi/[[path]].js`)。 101 | 102 | 2. **在 Cloudflare Pages 中创建项目**: 103 | 104 | - 登录到您的 Cloudflare 仪表板。 105 | - 导航到 "Workers & Pages"。 106 | - 点击 "Create application",然后选择 "Pages" 标签页。 107 | - 选择 "Connect to Git",并授权 Cloudflare 访问您的代码仓库。 108 | - 选择您要部署的仓库和生产分支 (例如 `main`)。 109 | 110 | 3. **配置构建设置**: 111 | 112 | - **Framework preset**: 选择 `无`。 113 | - **Build command**: 设置为 `yarn build` (或 `npm run build`,根据您的习惯)。 114 | - **Build output directory**: 确保设置为 `dist`。 115 | - **Root directory**: 保持默认或根据您的项目结构设置 (通常是仓库根目录)。 116 | - **注意**: 如果您的 Cloudflare Pages 项目面板提示非机密环境变量通过 `wrangler.toml` 文件进行管理,请确保在项目根目录的 `wrangler.toml` 文件中定义这些变量 (参考下方"Wrangler 配置文件"部分)。机密变量(Secrets)仍应通过 Cloudflare 仪表板管理。 117 | 118 | 4. **保存并部署**: 119 | - 点击 "Save and Deploy"。Cloudflare Pages 将拉取代码,执行构建命令,并将 `dist` 目录的内容和 `functions` 目录下的函数部署到其全球网络。 120 | - 部署完成后,您会得到一个 `*.pages.dev` 的子域名。您也可以后续配置自定义域名。 121 | - 您的 API 请求 (例如 `/appleapi/...`) 将会自动被路由到相应的 Pages Function 进行代理。 122 | 123 | ### 本地开发和调试 (使用 Wrangler) 124 | 125 | Cloudflare Wrangler CLI 允许您在本地模拟 Cloudflare Pages 环境,包括静态资源服务和 Functions。 126 | 127 | 1. **安装 Wrangler (如果尚未安装)**: 128 | 129 | - 推荐使用 npx (无需全局安装): `npx wrangler ...` 130 | - 或全局安装: `npm install -g wrangler` / `yarn global add wrangler` 131 | 132 | 2. **准备本地环境变量**: 133 | 134 | - 在项目根目录创建一个 `.dev.vars` 文件 (如果不存在)。 135 | - 添加本地开发时 Functions 需要的环境变量。此文件**不应**提交到 Git。 136 | ```ini 137 | # .dev.vars 138 | VITE_APP_STORE_URL="https://apps.apple.com" 139 | VITE_MT_SHOP_STATIC_URL="https://static.moutai519.com.cn" 140 | VITE_MT_APP_API_URL="https://app.moutai519.com.cn" 141 | # NODE_VERSION="22" # 如果本地测试也需要特定版本 142 | ``` 143 | - 确保 `.dev.vars` 已被添加到 `.gitignore` 文件中。 144 | 145 | 3. **Wrangler 配置文件 (`wrangler.toml`)**: 146 | 147 | - 项目中应包含一个 `wrangler.toml` 文件,基本配置如下: 148 | 149 | ```toml 150 | name = "imaotai-env-generator" # 与 Cloudflare Pages 项目名称一致 151 | compatibility_date = "2025-05-05" # 使用一个较新的日期 152 | pages_build_output_dir = "dist" 153 | 154 | # 定义环境变量,供 Pages Functions 在本地开发和生产环境中使用 155 | # 如果 Cloudflare Pages 面板指示环境变量由 wrangler.toml 管理, 156 | # 这里的 [vars] 部分将用于生产部署。 157 | [vars] 158 | VITE_APP_STORE_URL = "https://apps.apple.com" 159 | VITE_MT_SHOP_STATIC_URL = "https://static.moutai519.com.cn" 160 | VITE_MT_APP_API_URL = "https://app.moutai519.com.cn" 161 | # NODE_VERSION = "18" # 如果您的 Functions 需要特定 Node.js 版本 (示例) 162 | ``` 163 | 164 | - 此文件指导 Wrangler 如何在本地运行您的 Pages 项目。如果 Cloudflare 指示,此文件中的 `[vars]` 也会用于生产环境的变量配置。 165 | - **重要**: 如果您在 `wrangler.toml` 中定义了这些变量,请确保将此文件提交到您的 Git 仓库,以便 Cloudflare Pages 在部署时能够读取这些配置。 166 | 167 | 4. **本地调试流程**: 168 | 169 | - **方式一: 纯前端开发 (Vite Dev Server)** 170 | 171 | - 运行: `yarn dev` (或 `npm run dev`) 172 | - Vite 开发服务器将启动,提供热模块替换 (HMR)。API 请求将通过 `vite.config.ts` 中的 `server.proxy` 配置进行代理。 173 | - **注意**: 此模式不执行 `functions/` 目录下的 Cloudflare Pages Functions。 174 | 175 | - **方式二: 完整 Pages 环境模拟 (Wrangler)** 176 | 1. **构建前端**: `yarn build` (或 `npm run build`),生成 `dist/` 目录。 177 | 2. **启动 Wrangler**: `npx wrangler pages dev` 178 | - Wrangler 会从 `dist` 目录提供静态文件,并运行 `functions/` 目录下的 Functions。 179 | - 环境变量将从 `.dev.vars` 文件加载。 180 | - 通常服务会运行在 `http://localhost:8788`。 181 | 3. 在浏览器中访问 Wrangler 提供的地址进行测试。API 调用将由本地运行的 Pages Functions 处理。 182 | 183 | ### 注意事项 184 | 185 | - 部署到 Cloudflare Pages 后,原先项目中的 `server/` 目录 (包含 `app.js` 和 `package.json`) 和 `Dockerfile` 将不再需要,因为其功能已被 Cloudflare Pages 的静态资源服务和 Pages Functions 替代。 186 | - `vite.config.ts` 中的 `server.proxy` 配置仅用于本地 Vite 开发服务器,不影响 Cloudflare Pages 的生产部署。 187 |
188 | 189 | ## 免责声明 190 | 191 | 本项目涉及抓取接口数据,仅用于学习和交流目的。请注意以下几点: 192 | 193 | 1. **合法性和合规性**: 使用本项目时,请确保遵守所有相关法律法规以及服务条款。本项目的使用可能涉及到法律风险,用户需要对使用本项目的行为负责。 194 | 2. **数据隐私**: 本项目涉及对接口数据的抓取,用户需自行保证对其账号、数据和隐私的保护,避免泄露敏感信息。 195 | 3. **风险提示**: 由于抓取接口数据可能会受到系统限制或变更,本项目的正常运行和功能实现无法得到保证。使用本项目的风险由用户自行承担。 196 | 4. **第三方服务**: 本项目的部分功能可能依赖于第三方服务或接口,这些服务的变更或不可用可能会影响脚本的正常工作。 197 | 5. **学习和交流**: 本项目仅用于学习和交流目的,旨在帮助用户了解接口抓取和自动化处理的技术。请勿用于商业用途或其他非法活动。 198 | 6. **责任声明**: 本项目作者不对因使用本项目而产生的任何直接或间接损失负责。请用户在使用前充分理解相关风险,并确保合法合规使用。 199 | 200 | ## 许可证 201 | 202 | 本项目使用 [Apache-2.0 许可证](LICENSE) 进行许可。有关更多详细信息,请参阅 `LICENSE` 文件。 203 | -------------------------------------------------------------------------------- /src/views/generate/components/StepThree.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | 283 | 284 | 305 | -------------------------------------------------------------------------------- /src/views/generate/components/StepOne.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 254 | 255 | 275 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 254 | 255 | 292 | -------------------------------------------------------------------------------- /src/types/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 11 | const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] 12 | const computed: typeof import('vue')['computed'] 13 | const createApp: typeof import('vue')['createApp'] 14 | const customRef: typeof import('vue')['customRef'] 15 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 16 | const defineComponent: typeof import('vue')['defineComponent'] 17 | const effectScope: typeof import('vue')['effectScope'] 18 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 19 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 20 | const h: typeof import('vue')['h'] 21 | const inject: typeof import('vue')['inject'] 22 | const isProxy: typeof import('vue')['isProxy'] 23 | const isReactive: typeof import('vue')['isReactive'] 24 | const isReadonly: typeof import('vue')['isReadonly'] 25 | const isRef: typeof import('vue')['isRef'] 26 | const markRaw: typeof import('vue')['markRaw'] 27 | const nextTick: typeof import('vue')['nextTick'] 28 | const onActivated: typeof import('vue')['onActivated'] 29 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 30 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 31 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 32 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 33 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 34 | const onDeactivated: typeof import('vue')['onDeactivated'] 35 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 36 | const onMounted: typeof import('vue')['onMounted'] 37 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 38 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 39 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 40 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 41 | const onUnmounted: typeof import('vue')['onUnmounted'] 42 | const onUpdated: typeof import('vue')['onUpdated'] 43 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 44 | const provide: typeof import('vue')['provide'] 45 | const reactive: typeof import('vue')['reactive'] 46 | const readonly: typeof import('vue')['readonly'] 47 | const ref: typeof import('vue')['ref'] 48 | const resolveComponent: typeof import('vue')['resolveComponent'] 49 | const shallowReactive: typeof import('vue')['shallowReactive'] 50 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 51 | const shallowRef: typeof import('vue')['shallowRef'] 52 | const toRaw: typeof import('vue')['toRaw'] 53 | const toRef: typeof import('vue')['toRef'] 54 | const toRefs: typeof import('vue')['toRefs'] 55 | const toValue: typeof import('vue')['toValue'] 56 | const triggerRef: typeof import('vue')['triggerRef'] 57 | const unref: typeof import('vue')['unref'] 58 | const useAttrs: typeof import('vue')['useAttrs'] 59 | const useCssModule: typeof import('vue')['useCssModule'] 60 | const useCssVars: typeof import('vue')['useCssVars'] 61 | const useId: typeof import('vue')['useId'] 62 | const useLink: typeof import('vue-router')['useLink'] 63 | const useModel: typeof import('vue')['useModel'] 64 | const useRoute: typeof import('vue-router')['useRoute'] 65 | const useRouter: typeof import('vue-router')['useRouter'] 66 | const useSlots: typeof import('vue')['useSlots'] 67 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 68 | const watch: typeof import('vue')['watch'] 69 | const watchEffect: typeof import('vue')['watchEffect'] 70 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 71 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 72 | } 73 | // for type re-export 74 | declare global { 75 | // @ts-ignore 76 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 77 | import('vue') 78 | } 79 | 80 | // for vue template auto import 81 | import { UnwrapRef } from 'vue' 82 | declare module 'vue' { 83 | interface GlobalComponents {} 84 | interface ComponentCustomProperties { 85 | readonly EffectScope: UnwrapRef 86 | readonly ElMessage: UnwrapRef 87 | readonly ElMessageBox: UnwrapRef 88 | readonly computed: UnwrapRef 89 | readonly createApp: UnwrapRef 90 | readonly customRef: UnwrapRef 91 | readonly defineAsyncComponent: UnwrapRef 92 | readonly defineComponent: UnwrapRef 93 | readonly effectScope: UnwrapRef 94 | readonly getCurrentInstance: UnwrapRef 95 | readonly getCurrentScope: UnwrapRef 96 | readonly h: UnwrapRef 97 | readonly inject: UnwrapRef 98 | readonly isProxy: UnwrapRef 99 | readonly isReactive: UnwrapRef 100 | readonly isReadonly: UnwrapRef 101 | readonly isRef: UnwrapRef 102 | readonly markRaw: UnwrapRef 103 | readonly nextTick: UnwrapRef 104 | readonly onActivated: UnwrapRef 105 | readonly onBeforeMount: UnwrapRef 106 | readonly onBeforeRouteLeave: UnwrapRef 107 | readonly onBeforeRouteUpdate: UnwrapRef 108 | readonly onBeforeUnmount: UnwrapRef 109 | readonly onBeforeUpdate: UnwrapRef 110 | readonly onDeactivated: UnwrapRef 111 | readonly onErrorCaptured: UnwrapRef 112 | readonly onMounted: UnwrapRef 113 | readonly onRenderTracked: UnwrapRef 114 | readonly onRenderTriggered: UnwrapRef 115 | readonly onScopeDispose: UnwrapRef 116 | readonly onServerPrefetch: UnwrapRef 117 | readonly onUnmounted: UnwrapRef 118 | readonly onUpdated: UnwrapRef 119 | readonly onWatcherCleanup: UnwrapRef 120 | readonly provide: UnwrapRef 121 | readonly reactive: UnwrapRef 122 | readonly readonly: UnwrapRef 123 | readonly ref: UnwrapRef 124 | readonly resolveComponent: UnwrapRef 125 | readonly shallowReactive: UnwrapRef 126 | readonly shallowReadonly: UnwrapRef 127 | readonly shallowRef: UnwrapRef 128 | readonly toRaw: UnwrapRef 129 | readonly toRef: UnwrapRef 130 | readonly toRefs: UnwrapRef 131 | readonly toValue: UnwrapRef 132 | readonly triggerRef: UnwrapRef 133 | readonly unref: UnwrapRef 134 | readonly useAttrs: UnwrapRef 135 | readonly useCssModule: UnwrapRef 136 | readonly useCssVars: UnwrapRef 137 | readonly useId: UnwrapRef 138 | readonly useLink: UnwrapRef 139 | readonly useModel: UnwrapRef 140 | readonly useRoute: UnwrapRef 141 | readonly useRouter: UnwrapRef 142 | readonly useSlots: UnwrapRef 143 | readonly useTemplateRef: UnwrapRef 144 | readonly watch: UnwrapRef 145 | readonly watchEffect: UnwrapRef 146 | readonly watchPostEffect: UnwrapRef 147 | readonly watchSyncEffect: UnwrapRef 148 | } 149 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/views/update/index.vue: -------------------------------------------------------------------------------- 1 | 398 | 399 | 802 | 803 | 826 | --------------------------------------------------------------------------------