├── .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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ res }}
8 |
9 |
10 |
11 |
38 |
43 |
--------------------------------------------------------------------------------
/src/pages/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
--------------------------------------------------------------------------------
/src/pages/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | token: {{ appStore.accessToken }}
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/src/pages/web.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------