├── .dockerignore ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierrc ├── .types-syncrc.json ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── README.md ├── build ├── injectPlugin.ts ├── mpHooks.ts ├── preview.sh └── time.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── src ├── App.vue ├── api │ ├── api.d.ts │ ├── index.ts │ └── interceptors │ │ ├── error.ts │ │ ├── request.ts │ │ └── response.ts ├── main.ts ├── manifest.json ├── pages.json ├── pages │ ├── home.vue │ ├── login.vue │ ├── user.vue │ └── web.vue ├── router │ ├── index.ts │ └── interceptors.ts ├── store │ ├── app.ts │ └── index.ts ├── uni.scss └── utils │ ├── file.ts │ ├── index.ts │ ├── loading.ts │ ├── oss.ts │ └── uni.ts ├── tsconfig.json ├── tsconfig.node.json ├── types ├── uni.d.ts └── vite.d.ts ├── unocss.config.ts └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .husky 5 | 6 | stats.html 7 | README.md 8 | .idea 9 | .DS_Store 10 | .vscode 11 | Dockerfile 12 | .git 13 | .gitignore 14 | .editorconfig 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 告诉EditorConfig插件,这是根文件,不用继续往上查找 2 | root = true 3 | 4 | # 匹配全部文件 5 | [*] 6 | # 设置字符集 7 | charset = utf-8 8 | # 缩进风格,可选space、tab 9 | indent_style = space 10 | # 缩进的空格数 11 | indent_size = 2 12 | # 结尾换行符,可选lf、cr、crlf 13 | end_of_line = lf -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_OSS_HOST='' 2 | VITE_API_HOST='' 3 | VITE_MP_APPID='' 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | 4 | /.vscode 5 | 6 | /src/static 7 | /src/lib 8 | /src/wxcomponents 9 | /src/uni_modules 10 | 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // ESlint 检查配置 2 | module.exports = { 3 | root: true, 4 | parser: 'vue-eslint-parser', 5 | parserOptions: { 6 | parser: '@typescript-eslint/parser', 7 | ecmaVersion: 2020, 8 | sourceType: 'module', 9 | requireConfigFile: false, 10 | }, 11 | env: { 12 | browser: true, 13 | node: true, 14 | es6: true, 15 | }, 16 | extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 17 | globals: { 18 | uni: true, 19 | UniApp: true, 20 | wx: true, 21 | }, 22 | rules: { 23 | 'vue/multi-word-component-names': 1, 24 | 'no-unused-vars': [1, { args: 'after-used', argsIgnorePattern: '^_' }], 25 | '@typescript-eslint/ban-ts-comment': 1, 26 | '@typescript-eslint/no-explicit-any': 1, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.local 4 | 5 | .lh 6 | .vscode/* 7 | !.vscode/extensions.json 8 | !.vscode/settings.json 9 | .idea 10 | .DS_Store 11 | /types/autoImport.d.ts 12 | # pnpm-lock.yaml 13 | /stats.html 14 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | registry=https://registry.npmmirror.com 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "proseWrap": "preserve", 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "semi": false, 7 | "singleQuote": true, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid", 10 | "jsxBracketSameLine": true, 11 | "endOfLine": "lf" 12 | } 13 | -------------------------------------------------------------------------------- /.types-syncrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "sass", 4 | "eslint", 5 | "eslint-config-prettier", 6 | "eslint-plugin-prettier", 7 | "prettier", 8 | "rollup-plugin-visualizer", 9 | "lint-staged" 10 | ], 11 | "packagesManager": "pnpm" 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "antfu.unocss", 5 | "esbenp.prettier-vscode", 6 | "dbaeumer.vscode-eslint", 7 | "mikestead.dotenv", 8 | "mhutchie.git-graph", 9 | // "vue.vscode-typescript-vue-plugin", recommend take over mode 10 | "sdras.vue-vscode-snippets", 11 | "uni-helper.uni-app-snippets-vscode", 12 | "uni-helper.uni-app-schemas-vscode" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.format.semicolons": "remove", 3 | "editor.tabSize": 2, 4 | "editor.formatOnSave": true, 5 | "editor.wordWrapColumn": 120, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true, 8 | "source.fixAll.stylelint": true, 9 | "source.organizeImports": true 10 | }, 11 | "stylelint.validate": ["css", "scss", "vue"], 12 | "editor.formatOnSaveMode": "modificationsIfAvailable", 13 | "prettier.prettierPath": "./node_modules/prettier/index.cjs", 14 | "[vue]": { 15 | "editor.formatOnSave": false, 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[javascript]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[html]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[typescript]": { 25 | "editor.formatOnSave": false, 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[json]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[jsonc]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "files.exclude": { 35 | "**/.classpath": true, 36 | "**/.factorypath": true, 37 | "**/.idea": true, 38 | "**/.project": true, 39 | "**/.settings": true 40 | }, 41 | "files.eol": "\n", 42 | "files.autoSave": "onFocusChange", 43 | "files.insertFinalNewline": true, 44 | "eslint.enable": true, 45 | "eslint.run": "onSave", 46 | "eslint.format.enable": true, 47 | "eslint.lintTask.enable": true, 48 | "npm.packageManager": "pnpm", 49 | "search.exclude": { 50 | "**/dist": true, 51 | "**/node_modules": true 52 | }, 53 | "cSpell.words": ["extralight", "miniprogram", "nprogress", "pinia", "qrcode", "vant", "vite", "weapp", "wechat"], 54 | "todo-tree.general.tags": ["BUG", "HACK", "FIXME", "TODO", "XXX", "[ ]", "[x]", "todo"], 55 | "todo-tree.filtering.excludeGlobs": ["**/node_modules/**", "/workers/**"], 56 | "todo-tree.highlights.customHighlight": { 57 | "TODO": { 58 | "foreground": "#ffec41", 59 | "type": "text-and-comment" 60 | } 61 | }, 62 | "files.watcherExclude": { 63 | "/dist/": true, 64 | "/src/lib/": true, 65 | "/src/static/": true, 66 | "/src/uni_modules": true 67 | // "/src/wxcomponents/": true 68 | }, 69 | "files.associations": { 70 | "*.cjson": "jsonc", 71 | "*.wxss": "css", 72 | "*.wxs": "javascript", 73 | ".env.*": "dotenv" 74 | } 75 | // "scss.lint.unknownAtRules": "ignore" 76 | } 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | RUN npm -g i pnpm 4 | WORKDIR /app 5 | ENV TZ="Asia/Shanghai" 6 | 7 | ADD package.json .npmrc pnpm-lock.yaml .types-syncrc.json /app/ 8 | RUN pnpm i 9 | 10 | ADD . . 11 | CMD pnpm build 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uniapp 小程序脚手架 2 | ## vite, vue3, ts 3 | 4 | | 类别 | 库 | 5 | | -------- | ------------------------------------ | 6 | | 包管理器 | pnpm | 7 | | 网络请求 | uni-ajax | 8 | | 路由拦截 | uni-crazy-router | 9 | | 原子样式 | unocss, unocss-preset-weapp | 10 | | 代码校验 | husky, lint-staged, eslint, prettier | 11 | | 状态管理 | pinia, pinia-plugin-persist-uni | 12 | | 类型导入 | types-sync, unplugin-auto-import | 13 | | 时间处理 | dayjs | 14 | 15 | # 使用 16 | ``` 17 | npx degit ijntvwh/uniapp_vue3_ts my-project 18 | 19 | ``` 20 | #更新 uniapp 版本 21 | 22 | ``` 23 | npx @dcloudio/uvm 24 | 25 | ``` 26 | 27 | # 删除多余uni 28 | ``` 29 | pnpm remove @dcloudio/uni-automator @dcloudio/uni-mp-alipay @dcloudio/uni-mp-baidu @dcloudio/uni-mp-kuaishou @dcloudio/uni-mp-lark @dcloudio/uni-mp-qq @dcloudio/uni-mp-toutiao @dcloudio/uni-mp-jd @dcloudio/uni-quickapp-webview vue-i18n 30 | 31 | 32 | ``` 33 | 34 | # 注入project.config.json 35 | ## appid 36 | 配置环境变量VITE_MP_APPID,拦截uni插件mp-manifest-json实现注入 37 | ## projectname和description 38 | 读取根目录package.json中的name和version,以便ci提取,参考[uni-mp-ci](https://github.com/ijntvwh/uni-mp-ci) 39 | -------------------------------------------------------------------------------- /build/injectPlugin.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import { Plugin } from 'vite' 4 | 5 | export function injectPlugin(): Plugin { 6 | let envPluginId: string 7 | let envPluginVersion: string 8 | return { 9 | name: 'injectPlugin', 10 | configResolved(config) { 11 | if (config.define && config.define['process.env.UNI_PLATFORM'] !== '"mp-weixin"') return 12 | envPluginId = config.env.VITE_PLUGIN_ID 13 | envPluginVersion = config.env.VITE_PLUGIN_VERSION 14 | }, 15 | writeBundle(options) { 16 | const dir = options.dir! 17 | if (!envPluginId || !envPluginVersion) return 18 | const appJsonPath = path.resolve(dir, 'app.json') 19 | const appJson = fs.readJSONSync(appJsonPath) 20 | appJson.plugins = { 21 | myPlugin: { 22 | version: envPluginVersion, 23 | provider: envPluginId, 24 | }, 25 | } 26 | fs.writeJsonSync(appJsonPath, appJson) 27 | }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /build/mpHooks.ts: -------------------------------------------------------------------------------- 1 | import { JSON_JS_MAP } from '@dcloudio/uni-cli-shared/dist/constants' 2 | import fs from 'fs-extra' 3 | import path from 'path' 4 | import { Plugin } from 'vite' 5 | 6 | const JSON_KEY_MANIFEST = JSON_JS_MAP['manifest.json'] 7 | 8 | export function mpHooks(): Plugin { 9 | return { 10 | name: 'mp-hooks', 11 | configResolved(config) { 12 | // console.log('plugins', config.plugins.map(p => p.name)) 13 | if (config.define && config.define['process.env.UNI_PLATFORM'] !== '"mp-weixin"') return 14 | const envAppId = config.env.VITE_MP_APPID 15 | if (!envAppId) { 16 | console.log('环境变量 VITE_MP_APPID 未配置') 17 | } 18 | const p = config.plugins.find(p => p.name === 'uni:mp-manifest-json')! 19 | const transform1 = p.transform as any 20 | p.transform = function (...args) { 21 | const id = args[1] 22 | if (id && id.endsWith(JSON_KEY_MANIFEST)) { 23 | const jsonObj = JSON.parse(args[0]) 24 | // jsonObj.description = jsonObj.versionName 25 | if (envAppId) jsonObj['mp-weixin'].appid = envAppId 26 | return transform1.call(this, JSON.stringify(jsonObj), ...args.slice(1)) 27 | } 28 | return transform1.apply(this, args) 29 | } 30 | }, 31 | writeBundle(options) { 32 | const dir = options.dir! 33 | if (dir?.endsWith('/build/mp-weixin')) { 34 | const projectConfigPath = path.resolve(dir, 'project.config.json') 35 | const projectJson = fs.readJsonSync(projectConfigPath) 36 | const packageJson = fs.readJsonSync(path.resolve(process.cwd(), 'package.json')) 37 | projectJson.projectname = packageJson.name 38 | projectJson.description = packageJson.version 39 | fs.writeJsonSync(projectConfigPath, projectJson) 40 | console.log('writeBundle', projectJson) 41 | } 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /build/preview.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | PROJECT_DIR=$(cd $(dirname $0);cd ..;pwd) 4 | 5 | # TODO 配置cli环境 6 | WECHAT_CLI=/Applications/wechatwebdevtools.app/Contents/MacOS/cli 7 | 8 | $WECHAT_CLI preview --project $PROJECT_DIR/dist/dev/mp-weixin --qr-size small 9 | -------------------------------------------------------------------------------- /build/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export const buildTime = { 4 | name: 'build-time', 5 | closeBundle() { 6 | console.log(`-------------------- ${dayjs().format('HH:mm:ss')} --------------------`) 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniapp_vue3_ts", 3 | "version": "1.0.1", 4 | "scripts": { 5 | "dev:h5": "uni", 6 | "dev": "uni -p mp-weixin", 7 | "build": "uni build -p mp-weixin", 8 | "prepare": "husky install", 9 | "preinstall": "npx only-allow pnpm", 10 | "preview": "bash build/preview.sh", 11 | "postinstall": "types-sync", 12 | "docker": "docker build -t uni-mp . && docker run -v $(pwd)/dist:/app/dist --rm uni-mp" 13 | }, 14 | "lint-staged": { 15 | "*.{ts,js,vue,json}": "eslint" 16 | }, 17 | "dependencies": { 18 | "@dcloudio/uni-app": "3.0.0-3090820231124001", 19 | "@dcloudio/uni-app-plus": "3.0.0-3090820231124001", 20 | "@dcloudio/uni-components": "3.0.0-3090820231124001", 21 | "@dcloudio/uni-h5": "3.0.0-3090820231124001", 22 | "@dcloudio/uni-mp-weixin": "3.0.0-3090820231124001", 23 | "@dcloudio/uni-mp-xhs": "3.0.0-3090820231124001", 24 | "dayjs": "^1.11.10", 25 | "lodash": "^4.17.21", 26 | "pinia": "~2.0.36", 27 | "pinia-plugin-persist-uni": "^1.2.0", 28 | "postcss": "^8.4.32", 29 | "sass": "^1.69.5", 30 | "uni-ajax": "^2.5.1", 31 | "uni-crazy-router": "^1.1.3", 32 | "url-parse": "^1.5.10", 33 | "vue": "^3.3.10" 34 | }, 35 | "devDependencies": { 36 | "@dcloudio/types": "^3.4.3", 37 | "@dcloudio/uni-cli-shared": "3.0.0-3090820231124001", 38 | "@dcloudio/uni-stacktracey": "3.0.0-3090820231124001", 39 | "@dcloudio/vite-plugin-uni": "3.0.0-3090820231124001", 40 | "@types/fs-extra": "^11.0.4", 41 | "@types/lodash": "^4.14.202", 42 | "@types/url-parse": "^1.4.11", 43 | "@typescript-eslint/eslint-plugin": "^6.13.2", 44 | "@typescript-eslint/parser": "^6.13.2", 45 | "@unocss/transformer-directives": "^0.58.0", 46 | "@vue/runtime-core": "^3.3.10", 47 | "dotenv": "^16.3.1", 48 | "eslint": "^8.55.0", 49 | "eslint-config-prettier": "^9.1.0", 50 | "eslint-plugin-prettier": "^5.0.1", 51 | "eslint-plugin-vue": "^9.19.2", 52 | "fs-extra": "^11.2.0", 53 | "husky": "^8.0.3", 54 | "lint-staged": "^15.2.0", 55 | "miniprogram-api-typings": "^3.12.2", 56 | "prettier": "^3.1.0", 57 | "rollup-plugin-visualizer": "^5.10.0", 58 | "types-sync": "3.64.0", 59 | "typescript": "^5.3.3", 60 | "unocss": "^0.58.0", 61 | "unocss-preset-weapp": "^0.58.0", 62 | "unplugin-auto-import": "^0.17.2", 63 | "vite": "5.0.6", 64 | "vite-plugin-inspect": "^0.8.1" 65 | }, 66 | "engines": { 67 | "node": ">=18", 68 | "pnpm": ">=8" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/api/api.d.ts: -------------------------------------------------------------------------------- 1 | import 'uni-ajax' 2 | 3 | declare module 'uni-ajax' { 4 | type ReqCustomKey = 'noToast' | 'noLogin' | 'fullData' 5 | interface AjaxRequestConfig { 6 | custom?: ReqCustomKey[] 7 | } 8 | interface AjaxInvoke { 9 | (config?: AjaxRequestConfig): Promise 10 | (url?: string, data?: Data, config?: AjaxRequestConfig): Promise 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { navLogin, toastError } from '@/api/interceptors/error' 2 | import { injectToken } from '@/api/interceptors/request' 3 | import { extractResData } from '@/api/interceptors/response' 4 | import ajax, { AjaxRequestConfig, AjaxResponse, ReqCustomKey } from 'uni-ajax' 5 | 6 | const instance = ajax.create({ baseURL: import.meta.env.VITE_API_HOST }) 7 | 8 | const reqChain: ((_config: AjaxRequestConfig) => Promise)[] = [injectToken] 9 | const resChain: ((_res: any) => Promise)[] = [extractResData] 10 | const errChain: ((_err: Error | AjaxResponse) => Promise)[] = [navLogin, toastError] 11 | 12 | const onRejected = (err: Error | AjaxResponse) => errChain.reduce((pre, cur) => pre.catch(cur), Promise.reject(err)) 13 | 14 | instance.interceptors.request.use( 15 | config => reqChain.reduce((pre, cur) => pre.then(cur), Promise.resolve(config)).catch(onRejected), 16 | onRejected 17 | ) 18 | 19 | instance.interceptors.response.use( 20 | res => resChain.reduce((pre, cur) => pre.then(cur), Promise.resolve(res)).catch(onRejected), 21 | onRejected 22 | ) 23 | 24 | export const Api = instance 25 | 26 | export const hasCustomKey = (config: AjaxRequestConfig | null, key: ReqCustomKey): boolean | undefined => { 27 | return config?.custom?.includes(key) 28 | } 29 | -------------------------------------------------------------------------------- /src/api/interceptors/error.ts: -------------------------------------------------------------------------------- 1 | import { hasCustomKey } from '@/api' 2 | import { navTo } from '@/router' 3 | import { useAppStore } from '@/store/app' 4 | import { showToast } from '@/utils/uni' 5 | import { AjaxResponse } from 'uni-ajax' 6 | 7 | export const ERROR_OVERWRITE: Record = { 8 | // 重写错误代码对应的toast消息内容 9 | '003000': '不合法的Token', 10 | } 11 | 12 | export const toastError = (err: Error | AjaxResponse) => { 13 | console.log('toast err', err) 14 | if ((err as { errMsg: string })?.errMsg === 'request:fail abort') { 15 | // request abort 16 | return Promise.reject(err) 17 | } 18 | const isError = err instanceof Error 19 | if (isError || !hasCustomKey(err.config, 'noToast')) { 20 | const title = isError ? err.message : ERROR_OVERWRITE[err.data?.code] ?? (err.data?.msg || '系统错误') 21 | setTimeout(() => showToast(title), 100) 22 | } 23 | return Promise.reject(err) 24 | } 25 | 26 | export const navLogin = (err: Error | AjaxResponse) => { 27 | const errPromise = Promise.reject(err) 28 | if (err instanceof Error) return errPromise 29 | // TODO 未登录判断 30 | if (err.data?.code?.startsWith('003')) { 31 | useAppStore().updateToken() 32 | if (!hasCustomKey(err.config, 'noLogin')) 33 | return navTo('login') 34 | .then(() => errPromise) 35 | .catch(() => errPromise) 36 | } 37 | return errPromise 38 | } 39 | -------------------------------------------------------------------------------- /src/api/interceptors/request.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '@/store/app' 2 | import { AjaxRequestConfig } from 'uni-ajax' 3 | 4 | export const injectToken = (config: AjaxRequestConfig): Promise => { 5 | const appStore = useAppStore() 6 | const token = appStore.accessToken 7 | // TODO 请求中的token处理 8 | token && (config.header = { ...config.header, Authorization: `Bearer ${token}` }) 9 | return Promise.resolve(config) 10 | } 11 | -------------------------------------------------------------------------------- /src/api/interceptors/response.ts: -------------------------------------------------------------------------------- 1 | import { hasCustomKey } from '@/api' 2 | import { AjaxResponse } from 'uni-ajax' 3 | 4 | export type BaseResult = { data: T; code: number; msg: string } 5 | 6 | // TODO 数据解析 7 | export const extractResData = (res: AjaxResponse): Promise => { 8 | console.log('res', res) 9 | return res.data?.code === 200 10 | ? Promise.resolve(hasCustomKey(res.config, 'fullData') ? res.data : res.data.data) 11 | : Promise.reject(res) 12 | } 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import App from '@/App.vue' 2 | import { Router } from '@/router' 3 | import { Pinia } from '@/store' 4 | import { createSSRApp } from 'vue' 5 | 6 | import 'uno.css' 7 | 8 | export function createApp() { 9 | const app = createSSRApp(App) 10 | app.use(Pinia) 11 | app.use(Router) 12 | return { app, Pinia } 13 | } 14 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "appid": "", 4 | "description": "", 5 | "versionName": "1.0.0", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | "mp-weixin": { 9 | "appid": "", 10 | "libVersion": "latest", 11 | "setting": { 12 | "ignoreDevUnusedFiles": false, 13 | "ignoreUploadUnusedFiles": false, 14 | "compileHotReLoad": false, 15 | "es6": false, 16 | "enhance": true, 17 | "urlCheck": false 18 | }, 19 | "usingComponents": true 20 | }, 21 | "uniStatistics": { 22 | "enable": false 23 | }, 24 | "vueVersion": "3" 25 | } 26 | -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "path": "pages/home" 5 | }, 6 | { 7 | "path": "pages/login" 8 | }, 9 | { 10 | "path": "pages/user" 11 | }, 12 | { 13 | "path": "pages/web", 14 | "style": { 15 | "pageOrientation": "landscape" 16 | } 17 | } 18 | ], 19 | "globalStyle": { 20 | "navigationBarTextStyle": "black", 21 | "navigationBarTitleText": "uni-app", 22 | "navigationBarBackgroundColor": "#F8F8F8", 23 | "backgroundColor": "#F8F8F8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 43 | -------------------------------------------------------------------------------- /src/pages/login.vue: -------------------------------------------------------------------------------- 1 | 4 | 14 | -------------------------------------------------------------------------------- /src/pages/user.vue: -------------------------------------------------------------------------------- 1 | 7 | 17 | -------------------------------------------------------------------------------- /src/pages/web.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import pagesJson from '@/pages.json' 2 | import uniCrazyRouter from 'uni-crazy-router' 3 | import { loginCheck } from './interceptors' 4 | 5 | type NavToOptions = UniApp.NavigateToOptions | UniNamespace.NavigateToOptions 6 | type NavRedirectOptions = UniApp.RedirectToOptions | UniNamespace.RedirectToOptions 7 | type NavLaunchOptions = UniApp.ReLaunchOptions | UniNamespace.ReLaunchOptions 8 | type NavTabOptions = UniApp.SwitchTabOptions | UniNamespace.SwitchTabOptions 9 | 10 | type CommonOptions = NavToOptions | NavRedirectOptions | NavLaunchOptions | NavTabOptions 11 | 12 | export type NavKey = 'to' | 'redirect' | 'launch' | 'tab' 13 | export const navTo = (to: string | CommonOptions, type: NavKey = 'to'): Promise => { 14 | const NavFuncMap = { to: uni.navigateTo, redirect: uni.redirectTo, launch: uni.reLaunch, tab: uni.switchTab } 15 | const func = NavFuncMap[type] 16 | if (typeof to === 'string') { 17 | // 自动添加 /pages/ 的前缀 18 | const url = /^\/?pages\//.test(to) ? to : `/pages/${to}` 19 | // @ts-ignore 20 | return func({ url }) 21 | } 22 | // @ts-ignore 23 | return func(to) 24 | } 25 | 26 | export const navBack = () => { 27 | const pageStack = getCurrentPages() 28 | const curPage = pageStack.at(-1) 29 | const homePath = pagesJson.pages[0].path 30 | if (pageStack.length === 1) { 31 | if (curPage?.route !== homePath) navTo(`/${homePath}`, 'launch') 32 | return 33 | } 34 | uni.navigateBack() 35 | } 36 | 37 | const installFunc = uniCrazyRouter.install 38 | uniCrazyRouter.install = (...args: any[]) => { 39 | installFunc(...args) 40 | loginCheck(uniCrazyRouter) 41 | } 42 | export const Router = uniCrazyRouter 43 | -------------------------------------------------------------------------------- /src/router/interceptors.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '@/store/app' 2 | import { navTo } from '@/router' 3 | import uniCrazyRouter from 'uni-crazy-router' 4 | 5 | const authPages = ['user'] 6 | const authPages1 = authPages.map(s => `pages/${s}`) 7 | 8 | export const loginCheck = (router: uniCrazyRouter) => { 9 | const appStore = useAppStore() 10 | router.beforeEach(async (to, from, next) => { 11 | if (!appStore.accessToken && authPages1.includes(to.url)) { 12 | from?.url !== 'pages/login' && router.afterNotNext(() => navTo('login')) 13 | return 14 | } 15 | next() 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/store/app.ts: -------------------------------------------------------------------------------- 1 | import { uniStorage } from '@/store' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useAppStore = defineStore('app', { 5 | state: () => ({ accessToken: '' }), 6 | // getters: {}, 7 | actions: { 8 | updateToken(s = '') { 9 | this.accessToken = s 10 | }, 11 | }, 12 | persist: { 13 | enabled: true, 14 | strategies: [ 15 | { 16 | storage: uniStorage, 17 | paths: ['accessToken'], 18 | }, 19 | ], 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import piniaPersist from 'pinia-plugin-persist-uni' 3 | 4 | export const uniStorage: Storage = { 5 | length: 0, 6 | key: () => '', 7 | getItem(key: string): string | null { 8 | return uni.getStorageSync(key) 9 | }, 10 | setItem(key: string, value: string) { 11 | uni.setStorageSync(key, value) 12 | }, 13 | clear() { 14 | uni.clearStorageSync() 15 | }, 16 | removeItem(key: string) { 17 | uni.removeStorageSync(key) 18 | }, 19 | } 20 | 21 | const pinia = createPinia() 22 | pinia.use(piniaPersist) 23 | export const Pinia = pinia 24 | -------------------------------------------------------------------------------- /src/uni.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ijntvwh/uniapp_vue3_ts/56d959d7af1d1f1a4be9956232624ed430dc6f88/src/uni.scss -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import parseURL from 'url-parse' 3 | 4 | const fsm = wx.getFileSystemManager() 5 | const WX_FILE_PREFIX = wx.env.USER_DATA_PATH 6 | const URL_PREFIX = 'url' 7 | type WxPath = string 8 | 9 | function checkWxFile(filePath: WxPath) { 10 | return new Promise((resolve, reject) => 11 | fsm.getFileInfo({ filePath, success: () => resolve(filePath), fail: reject }) 12 | ) 13 | } 14 | 15 | export function writeFile(filePath: string, data: string | ArrayBuffer, encoding: 'base64' | 'binary') { 16 | return new Promise((resolve, reject) => 17 | fsm.writeFile({ filePath, data, encoding, success: () => resolve(), fail: reject }) 18 | ) 19 | } 20 | 21 | export function toWxPath(p: string): WxPath { 22 | if (p.startsWith(WX_FILE_PREFIX)) return p as WxPath 23 | return `${WX_FILE_PREFIX}${p.startsWith('/') ? '' : '/'}${p}` as WxPath 24 | } 25 | 26 | function checkWxDir(filePath: WxPath): Promise { 27 | const dirPath = filePath.substring(0, filePath.lastIndexOf('/')) 28 | return new Promise((resolve, reject) => 29 | fsm.stat({ 30 | path: dirPath, 31 | success(res: WechatMiniprogram.StatSuccessCallbackResult) { 32 | const stats = res.stats as WechatMiniprogram.Stats 33 | // console.log('stats', stats) 34 | stats.isDirectory() ? resolve() : reject(new Error('dir is File')) 35 | }, 36 | fail() { 37 | fsm.mkdir({ dirPath, recursive: true, success: () => resolve(), fail: reject }) 38 | }, 39 | }) 40 | ) 41 | } 42 | 43 | function downWxFile(url: string, filePath: WxPath) { 44 | return checkWxDir(filePath).then( 45 | () => 46 | new Promise((resolve, reject) => { 47 | wx.downloadFile({ url, filePath, success: () => resolve(filePath), fail: reject }) 48 | }) 49 | ) 50 | } 51 | 52 | let last = 0 53 | const CLEAN_CACHE_INTERVAL = 10 54 | const URL_MAX = 20 55 | function cleanCacheFiles(prefix = URL_PREFIX, count = URL_MAX) { 56 | const now = dayjs().unix() 57 | if (last && now - last < CLEAN_CACHE_INTERVAL) return Promise.resolve() 58 | last = now 59 | const path = toWxPath(prefix) 60 | return new Promise(resolve => { 61 | fsm.stat({ 62 | path, 63 | recursive: true, 64 | success: ({ stats }) => { 65 | try { 66 | const statsArray = stats as unknown as [{ path: string; stats: WechatMiniprogram.Stats }] 67 | const statsFile = statsArray.filter(f => f.stats.isFile()) 68 | statsFile.sort((a, b) => b.stats.lastAccessedTime - a.stats.lastAccessedTime) 69 | statsFile.slice(count).forEach(s => fsm.unlink({ filePath: path + s.path })) 70 | } finally { 71 | resolve() 72 | } 73 | }, 74 | fail: () => resolve(), 75 | }) 76 | }) 77 | } 78 | 79 | function urlToPath(s: string, prefix: string) { 80 | const { hostname, pathname } = parseURL(s, false) 81 | return [prefix, hostname.replaceAll('.', '_'), pathname.substring(1)].join('/') 82 | } 83 | 84 | export const loadUrl = (url: string, prefix = URL_PREFIX) => { 85 | const pCache = prefix === URL_PREFIX ? cleanCacheFiles() : Promise.resolve() 86 | const wxFile = toWxPath(urlToPath(url, prefix)) 87 | return pCache.then(() => checkWxFile(wxFile)).catch(() => downWxFile(url, wxFile)) 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const isDev = import.meta.env.VITE_USER_NODE_ENV === 'development' 2 | export const isIOS = uni.getSystemInfoSync().platform === 'ios' 3 | export const pageCtx = () => getCurrentPages<{ $routeParams: Record }>().at(-1) 4 | export const pageParams = () => pageCtx()?.$routeParams ?? {} 5 | -------------------------------------------------------------------------------- /src/utils/loading.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'vue' 2 | 3 | export const watchShow = (ing: Ref, title = '加载中') => 4 | watchEffect(() => { 5 | ing.value ? uni.showLoading({ title }) : uni.hideLoading() 6 | }) 7 | 8 | export const wrapPromise = (p: () => Promise, ingRef: Ref) => { 9 | return ingCheck(ingRef) 10 | .then(() => (ingRef.value = true)) 11 | .then(p) 12 | .finally(() => (ingRef.value = false)) 13 | } 14 | 15 | export const ingCheck = (ingRef: Ref) => { 16 | return ingRef.value ? Promise.reject('ing') : Promise.resolve() 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/oss.ts: -------------------------------------------------------------------------------- 1 | const HOST = import.meta.env.VITE_OSS_HOST 2 | export const ossURL = (path: string) => `${HOST}/${path}` 3 | -------------------------------------------------------------------------------- /src/utils/uni.ts: -------------------------------------------------------------------------------- 1 | export const uniShowModal = uni.showModal as UniPromisify 2 | export const uniGetUserProfile = uni.getUserProfile as UniPromisify 3 | export const uniLogin = uni.login as UniPromisify 4 | export const uniNavigateTo = uni.navigateTo as UniPromisify 5 | export const uniStartCompass = uni.startCompass as UniPromisify 6 | export const uniOpenLocation = uni.openLocation as UniPromisify 7 | export const uniGetImageInfo = uni.getImageInfo as UniPromisify 8 | export const uniCanvasToTempFilePath = 9 | uni.canvasToTempFilePath as UniPromisify 10 | export const uniSaveImageToPhotosAlbum = 11 | uni.saveImageToPhotosAlbum as UniPromisify 12 | export const uniGetSetting = uni.getSetting as UniPromisify 13 | export const uniAuthorize = uni.authorize as UniPromisify 14 | 15 | export const showToast = (title: string, icon: 'none' | 'success' | 'loading' | 'error' = 'none') => { 16 | if (!title) return Promise.resolve() 17 | return uni.showToast({ title, icon, duration: 2000 }) 18 | } 19 | 20 | export const queryCanvas = (selector: string): Promise => 21 | new Promise(resolve => 22 | uni 23 | .createSelectorQuery() 24 | .select(selector) 25 | .node(res => resolve(res.node)) 26 | .exec() 27 | ) 28 | 29 | type PermScope = keyof UniNamespace.AuthSetting extends `scope.${infer T}` ? `${T}` : never 30 | 31 | export function requestPerm(perm: PermScope, content: string) { 32 | const scope: keyof UniNamespace.AuthSetting = `scope.${perm}` 33 | return uniGetSetting({}) 34 | .then(res => !res.authSetting[scope] && uniAuthorize({ scope })) 35 | .catch(err => { 36 | uniShowModal({ title: '', content }).then(res => res.confirm && uni.openSetting({})) 37 | return Promise.reject(err) 38 | }) 39 | } 40 | 41 | export const wxLoginCode = () => 42 | new Promise((resolve, reject) => 43 | wx.login({ 44 | success: (res: WechatMiniprogram.LoginSuccessCallbackResult) => resolve(res.code), 45 | fail: reject, 46 | }) 47 | ) 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["src/*"], 5 | "#/*": ["types/*"] 6 | }, 7 | "baseUrl": "./", 8 | "forceConsistentCasingInFileNames": true, 9 | "target": "esnext", 10 | "useDefineForClassFields": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "strict": true, 14 | "jsx": "preserve", 15 | "sourceMap": true, 16 | "resolveJsonModule": true, 17 | "esModuleInterop": true, 18 | "lib": ["esnext", "dom"], 19 | "types": ["@dcloudio/types", "pinia-plugin-persist-uni", "miniprogram-api-typings"] 20 | }, 21 | "include": ["types/*.d.ts", "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "strict": true, 5 | "composite": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": ["vite.config.ts", "build/*.ts", "ci/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /types/uni.d.ts: -------------------------------------------------------------------------------- 1 | type UniPromisify = T extends { success?: (_s: infer S) => void } 2 | ? (_options: Omit) => Promise 3 | : never 4 | -------------------------------------------------------------------------------- /types/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import transformerDirectives from '@unocss/transformer-directives' 2 | import presetWeapp from 'unocss-preset-weapp' 3 | import { transformerClass } from 'unocss-preset-weapp/transformer' 4 | 5 | export default { 6 | presets: [ 7 | // https://github.com/MellowCo/unocss-preset-weapp 8 | presetWeapp(), 9 | ], 10 | shortcuts: [ 11 | { 12 | screen: 'w-screen h-screen', 13 | 'flex-center': 'flex justify-center items-center', 14 | }, 15 | ], 16 | 17 | transformers: [ 18 | // https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerClass 19 | transformerClass(), 20 | transformerDirectives(), 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import uni from '@dcloudio/vite-plugin-uni' 2 | import { visualizer } from 'rollup-plugin-visualizer' 3 | import unocss from 'unocss/vite' 4 | import autoImport from 'unplugin-auto-import/vite' 5 | import { defineConfig, loadEnv } from 'vite' 6 | import Inspect from 'vite-plugin-inspect' 7 | import { mpHooks } from './build/mpHooks' 8 | import { buildTime } from './build/time' 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig(({ mode }) => { 12 | const env = loadEnv(mode, process.cwd()) 13 | console.log('env', mode, env) 14 | const pImport = autoImport({ imports: ['vue', 'pinia'], dts: 'types/autoImport.d.ts' }) 15 | const pInspect = Inspect({ build: env.VITE_USER_NODE_ENV === 'production', outputDir: 'dist/.vite-inspect' }) 16 | return { 17 | plugins: [ 18 | // 根据环境变量注入appid, 根据package.json的name和version写入project.config.json 19 | mpHooks(), 20 | uni(), 21 | unocss(), 22 | visualizer(), 23 | pImport, 24 | buildTime, 25 | pInspect, 26 | ], 27 | resolve: { 28 | alias: { '@': '/src/' }, 29 | extensions: ['.mjs', '.js', '.ts', '.json', '.vue'], 30 | }, 31 | esbuild: { 32 | drop: mode === 'production' ? ['console', 'debugger'] : [], 33 | }, 34 | } 35 | }) 36 | --------------------------------------------------------------------------------