diff --git a/.husky/commit-msg b/.husky/commit-msg index fd2bf70..23ce835 100644 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1 @@ -npx --no-install commitlint --edit $1 +#npx --no-install commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index a3ec6f2..ce4711a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ echo "Running pre-commit hook..." -pnpm run lint:lint-staged +#pnpm run lint:lint-staged diff --git a/.husky/uniapp从0到1.md b/.husky/uniapp从0到1.md new file mode 100644 index 0000000..a9f14dc --- /dev/null +++ b/.husky/uniapp从0到1.md @@ -0,0 +1,2064 @@ + + +## 环境准备 + +[vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) 是一个通过 `vue-cli ` 构建的跨移动端脚手架模板,结合了 `uniapp`、`vue3 `和 `typescript`。在开始之前,需要准备以下环境。如果环境准备OK,请忽略本节。 + +### 安装 Node + +> `Node.js` 是运行 JavaScript 代码的环境,也是 `npm` 包管理器的依赖。 + +打开 [Node.js 官方下载页面](https://nodejs.org/zh-cn/download/prebuilt-installer),根据你的操作系统选择合适的版本进行下载,**推荐安装 LTS 版本**,这是长期支持版本,适合开发环境,比如这里选择 `v20.18.0(LTS) ` 版本。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024220508312.png) + +下载之后,双击安装包根据提示安装,通过以下命令检查是否成功安装: + +```bash +node -v +``` + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024224347056.png) + + + +### 安装 VSCode + +>`VSCode` 是一款非常流行的代码编辑器,特别适合前端开发。 + +访问 [Visual Studio Code 官方网站](https://code.visualstudio.com/) ,根据你的操作系统下载相应版本的 `VSCode` ,下载完成后,双击安装程序并按照提示完成安装。 + + + + ### 安装 vue-cli + +> `Vue CLI` 是 Vue.js 的命令行工具,能够快速创建、开发、构建 Vue.js 项目。 + +打开终端或命令提示符, 使用 `npm` 全局安装 `Vue CLI`: + +``` +npm install -g @vue/cli +``` + +安装完成后,检查 `Vue CLI` 是否安装成功: + +```bash +vue --version +``` + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024213719105.png) + +## 创建项目 + +### 初始化项目 + +按照 [uni-app 官方文档](https://uniapp.dcloud.net.cn/quickstart-cli.html#创建uni-app) 的步骤,通过 `vue-cli` 创建 `uni-app` + `vue` + `typescript` 脚手架: + +```bash +npx degit dcloudio/uni-preset-vue#vite-ts vue-uniapp-template +``` + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240917083025350.png) + +如果使用命令创建失败,可以通过 Gitee 下载 ZIP 包:[vite-ts 分支](https://gitee.com/dcloud/uni-preset-vue/tree/vite-ts)。 + +### 配置编译器 + +默认生成的 `TypeScript ` 编译器配置文件 `tsconfig.json` 中继承的 `@vue/tsconfig/tsconfig.json` 文件不存在。因此,你需要移除此继承配置并添加相应的编译设置。 + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240923143412332.png) + +根据 [TypeScript 官方配置文档](https://www.typescriptlang.org/tsconfig/),调整后的完整配置如下: + +```json +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "node", + "target": "esnext", + "allowJs": true, + "skipLibCheck": true, + "strict": true, + + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "lib": ["esnext", "dom"], + "types": ["@dcloudio/types"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules", "dist"] +} +``` + +- `"module": "esnext"`: 指定模块系统为 `ESNext`,即最新的 ECMAScript 模块系统,支持 `import.meta` 和其他最新的特性。 +- `"moduleResolution": "node"`: 模块解析策略,通常设置为 `Node` 以支持 Node.js 风格的模块解析。 +- `"target": "esnext"`: 将目标 JavaScript 版本设置为 `ESNext`,编译输出现代浏览器能够支持的最新特性代码。 +- `"allowJs": true`: 允许 TypeScript 编译器处理 `.js` 文件,混合使用 TypeScript 和 JavaScript 文件。 +- `"skipLibCheck": true`: 跳过库文件的类型检查,提升编译速度。 +- `"strict": true` : 启用所有严格类型检查选项。 + +### 启动项目 + +创建完成后,使用 `VSCode` 打开项目并启动: + +```bash +# 安装依赖 +pnpm install +# 启动项目 +pnpm run dev:h5 +``` + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240917085244593.png) + +项目启动后,访问 http://localhost:5173 预览效果: + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240917085841605.png) + + + +## 代码规范配置 + +为了保证项目代码的规范性和一致性,可以为项目配置 `ESLint`、`Stylelint`、`Prettier` 以及 `Husky`,从而确保代码质量和开发流程的一致性。 + + ### 集成 ESLint + +`ESLint` 是一款 JavaScript 和 TypeScript 的代码规范工具,能够帮助开发团队保持代码风格一致并减少常见错误。 + +**ESLint 中文网**:[https://eslint.nodejs.cn/](https://eslint.nodejs.cn/) + +#### 安装插件 + +VSCode 插件市场搜索 ESLint 插件并安装 + +![](https://s2.loli.net/2023/04/17/C3mV14KT2vHo6ZA.png) + +#### 配置 ESLint + +通过以下命令快速生成 ESLint 配置文件: + +```bash +npx eslint --init +``` + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240917202516052.png) + +执行该命令后,ESLint 会通过交互式问题的方式,帮助生成配置文件。针对 9.x 版本,默认会生成基于 Flat Config 格式的 `eslint.config.mjs` 文件,与之前的 `.eslintrc` 格式有所不同。 + +默认生成的 `eslint.config.mjs` 文件如下所示: + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240917221647303.png) + +在此基础上,可以根据项目的需求进行一些定制化配置,例如添加忽略规则或自定义的特殊规则。 + +```js +import globals from "globals"; // 全局变量配置 +import pluginJs from "@eslint/js"; // JavaScript 的推荐配置 +import tseslint from "typescript-eslint"; // TypeScript 配置 +import pluginVue from "eslint-plugin-vue"; // Vue 配置 + +export default [ + {files: ["**/*.{js,mjs,cjs,ts,vue}"]}, // 校验的文件类型 + {languageOptions: { globals: {...globals.browser , ...globals.node} }}, // 浏览器/Node环境全局变量 + pluginJs.configs.recommended, // JavaScript 推荐配置 + ...tseslint.configs.recommended, // TypeScript 推荐配置 + ...pluginVue.configs["flat/essential"], // Vue 推荐配置 + { files: ["**/*.vue"], languageOptions: { parserOptions: { parser: tseslint.parser } } }, // 对 .vue 文件使用 TypeScript 解析器 + + // 添加忽略的文件或目录 + { + ignores: [ + "/dist", + "/public", + "/node_modules", + "**/*.min.js", + "**/*.config.mjs", + "**/*.tsbuildinfo", + "/src/manifest.json", + ] + }, + + // 自定义规则 + { + rules: { + quotes: ["error", "double"], // 强制使用双引号 + "quote-props": ["error", "always"], // 强制对象的属性名使用引号 + semi: ["error", "always"], // 要求使用分号 + indent: ["error", 2], // 使用两个空格进行缩进 + "no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行 + "no-trailing-spaces": "error", // 不允许行尾有空格 + + // TypeScript 规则 + "@typescript-eslint/no-explicit-any": "off", // 禁用 no-explicit-any 规则,允许使用 any 类型 + "@typescript-eslint/explicit-function-return-type": "off", // 不强制要求函数必须明确返回类型 + "@typescript-eslint/no-empty-interface": "off", // 禁用 no-empty-interface 规则,允许空接口声明 + "@typescript-eslint/no-empty-object-type": "off", // 允许空对象类型 + + // Vue 规则 + "vue/multi-word-component-names": "off", // 关闭多单词组件名称的限制 + "vue/html-indent": ["error", 2], // Vue 模板中的 HTML 缩进使用两个空格 + "vue/no-v-html": "off", // 允许使用 v-html (根据实际项目需要) + }, + }, +]; +``` + +#### 添加 ESLint 脚本 + +为了方便使用 ESLint,可以在 `package.json` 中添加 `lint` 脚本命令: + +```json +{ + "scripts": { + "lint:eslint": "eslint --fix ./src" + } +} +``` + +此脚本会自动修复符合 ESLint 规则的代码问题,并输出检查结果。 + +#### 测试效果 + +在 `App.vue` 文件中声明一个未使用的变量,并运行 `pnpm run lint:eslint`,可以看到 ESLint 提示该变量未使用。如下图所示: + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240917235501951.png) + +#### 推荐配置 + +安装 Vue 文件解析器 `vue-eslint-parser`: + +```bash +pnpm add -D vue-eslint-parser +``` + +针对不同文件配置插件和解析器: + +```javascript +// eslint.config.mjs +import globals from "globals"; +import js from "@eslint/js"; + +// ESLint 核心插件 +import pluginVue from "eslint-plugin-vue"; +import pluginTypeScript from "@typescript-eslint/eslint-plugin"; + +// Prettier 插件及配置 +import configPrettier from "eslint-config-prettier"; +import pluginPrettier from "eslint-plugin-prettier"; + +// 解析器 +import * as parserVue from "vue-eslint-parser"; +import * as parserTypeScript from "@typescript-eslint/parser"; + +// 定义 ESLint 配置 +export default [ + // 通用 JavaScript/TypeScript 配置 + { + ...js.configs.recommended, + ignores: [ + "/dist", + "/public", + "/node_modules", + "**/*.min.js", + "**/*.config.mjs", + "**/*.tsbuildinfo", + "/src/manifest.json", + ], + languageOptions: { + globals: { + ...globals.browser, // 浏览器变量 (window, document 等) + ...globals.node, // Node.js 变量 (process, require 等) + }, + }, + plugins: { + prettier: pluginPrettier, + }, + rules: { + ...configPrettier.rules, + ...pluginPrettier.configs.recommended.rules, + "no-debug": "off", // 允许使用 debugger + "prettier/prettier": [ + "error", + { + endOfLine: "auto", // 解决换行符冲突 + }, + ], + }, + }, + + // TypeScript 配置 + { + files: ["**/*.?([cm])ts"], + languageOptions: { + parser: parserTypeScript, + parserOptions: { + sourceType: "module", + }, + }, + plugins: { + "@typescript-eslint": pluginTypeScript, + }, + rules: { + ...pluginTypeScript.configs.recommended.rules, + "@typescript-eslint/no-explicit-any": "off", // 允许使用 any + "@typescript-eslint/no-empty-function": "off", // 允许空函数 + "@typescript-eslint/no-empty-object-type": "off", // 允许空对象类型 + "@typescript-eslint/consistent-type-imports": [ + "error", + { disallowTypeAnnotations: false, fixStyle: "inline-type-imports" }, + ], // 统一类型导入风格 + }, + }, + + // TypeScript 声明文件的特殊配置 + { + files: ["**/*.d.ts"], + rules: { + "eslint-comments/no-unlimited-disable": "off", // 关闭 eslint 注释相关规则 + "unused-imports/no-unused-vars": "off", // 忽略未使用的导入 + }, + }, + + // JavaScript (commonjs) 配置 + { + files: ["**/*.?([cm])js"], + rules: { + "@typescript-eslint/no-var-requires": "off", // 允许 require + }, + }, + + // Vue 文件配置 + { + files: ["**/*.vue"], + languageOptions: { + parser: parserVue, + parserOptions: { + parser: "@typescript-eslint/parser", + sourceType: "module", + }, + }, + plugins: { + vue: pluginVue, + }, + processor: pluginVue.processors[".vue"], + rules: { + ...pluginVue.configs["vue3-recommended"].rules, + "vue/no-v-html": "off", // 允许 v-html + "vue/require-default-prop": "off", // 允许没有默认值的 prop + "vue/multi-word-component-names": "off", // 关闭组件名称多词要求 + "vue/html-self-closing": [ + "error", + { + html: { void: "always", normal: "always", component: "always" }, + svg: "always", + math: "always", + }, + ], // 自闭合标签 + }, + }, +]; +``` + + + +### 集成 Prettier + +Prettier 是一个代码格式化工具,能够和 ESLint 配合使用,确保代码风格统一。 + +**prettier 中文网**:[https://prettier.nodejs.cn/](https://prettier.nodejs.cn/) + +#### 安装插件 + +VSCode 插件市场搜索 `Prettier - Code formatter` 插件安装 + +![](https://s2.loli.net/2023/02/12/cBHDxNGzCaoeqvW.png) + +#### 安装依赖 + +```bash +pnpm install -D prettier eslint-config-prettier eslint-plugin-prettier +``` + +- **prettier**:主要的 Prettier 格式化库。 + +- **eslint-plugin-prettier**:将 Prettier 的规则作为 ESLint 的规则来运行。 + +- **eslint-config-prettier**:禁用所有与格式相关的 ESLint 规则,以避免和 Prettier 的冲突。 + +#### 配置 Prettier + +项目根目录下新建配置文件 `prettier.config.mjs`,添加常用规则: + +```js +export default { + printWidth: 100, // 每行最多字符数量,超出换行(默认80) + tabWidth: 2, // 缩进空格数,默认2个空格 + useTabs: false, // 指定缩进方式,空格或tab,默认false,即使用空格 + semi: true, // 使用分号 + singleQuote: false, // 使用单引号 (true:单引号;false:双引号) + trailingComma: 'all', // 末尾使用逗号 +}; +``` + +#### 配置忽略文件 + +项目根目录新建 `.prettierignore` 文件指定 Prettier 不需要格式化的文件和文件夹 + +```bash +# .prettierignore +node_modules +dist +public +*.min.js +``` + +#### 添加格式化脚本 + +在 `package.json` 文件中添加: + +```json +{ + "scripts": { + "format": "prettier --write ./src" + } +} +``` + +#### 保存自动格式化 + +打开 VSCode 的 `File` → `Preferences` → `Settings`,然后选择 `Open Settings (JSON)`,添加以下配置 + +```json +{ + "editor.formatOnSave": true, // 保存格式化文件 + "editor.defaultFormatter": "esbenp.prettier-vscode" // 指定 prettier 为所有文件默认格式化器 +} +``` + +#### 测试 + +下图演示了保存时的自动格式化效果,展示了代码中引号和换行的自动调整: + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/PixPin_2024-09-18_15-45-29.gif) + + + +### 集成 Stylelint + +Stylelint 一个强大的 CSS linter(检查器),可帮助您避免错误并强制执行约定。 + +**Stylelint 官网**:[https://stylelint.io/](https://stylelint.io/) + +#### 安装插件 + +VSCode 插件搜索 `Stylelint` 并安装 + +![](https://s2.loli.net/2023/04/08/azfOTp4K12c3PvQ.png) + + + +#### 安装依赖 + +```bash +pnpm install -D postcss postcss-html postcss-scss stylelint stylelint-config-recommended stylelint-config-recommended-scss stylelint-config-recommended-vue stylelint-config-recess-order stylelint-config-html stylelint-prettier +``` + +| 依赖 | 说明 | 备注 | +| --------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| postcss | CSS 解析工具,允许使用现代 CSS 语法并将其转换为兼容的旧语法 | | +| postcss-html | 解析 HTML (类似 HTML) 的 PostCSS 语法 | [postcss-html 文档](https://github.com/gucong3000/postcss-html) | +| postcss-scss | PostCSS 的 SCSS 解析器 | [postcss-scss 文档](https://github.com/postcss/postcss-scss),支持 CSS 行类注释 | +| stylelint | stylelint 核心库 | [stylelint ](https://stylelint.io/) | +| stylelint-config-standard | Stylelint 标准共享配置 | [stylelint-config-standard 文档](https://github.com/stylelint/stylelint-config-standard) | +| stylelint-config-recommended | | | +| stylelint-config-recommended-scss | 扩展 stylelint-config-recommended 共享配置并为 SCSS 配置其规则 | [stylelint-config-recommended-scss 文档](https://github.com/stylelint-scss/stylelint-config-recommended-scss) | +| stylelint-config-recommended-vue | 扩展 stylelint-config-recommended 共享配置并为 Vue 配置其规则 | [stylelint-config-recommended-vue 文档](https://github.com/ota-meshi/stylelint-config-recommended-vue) | +| stylelint-config-recess-order | 提供优化样式顺序的配置 | [CSS 书写顺序规范](https://jingyan.baidu.com/article/647f0115cf48957f2148a8a3.html) | +| stylelint-config-html | 共享 HTML (类似 HTML) 配置,捆绑 postcss-html 并对其进行配置 | [stylelint-config-html 文档](https://github.com/ota-meshi/stylelint-config-html) | +| stylelint-prettier | | | + + + +#### 配置 Stylelint + +根目录新建 `.stylelintrc.cjs` 文件,配置如下: + +```javascript +{ + "extends": [ + "stylelint-config-recommended", + "stylelint-config-recommended-scss", + "stylelint-config-recommended-vue/scss", + "stylelint-config-html/vue", + "stylelint-config-recess-order" + ], + "plugins": ["stylelint-prettier"], + "overrides": [ + { + "files": ["**/*.{vue,html}"], + "customSyntax": "postcss-html" + }, + { + "files": ["**/*.{css,scss}"], + "customSyntax": "postcss-scss" + } + ], + + "rules": { + "import-notation": "string", + "selector-class-pattern": null, + "custom-property-pattern": null, + "keyframes-name-pattern": null, + "no-descending-specificity": null, + "no-empty-source": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global", "export", "deep"] + } + ], + "unit-no-unknown": [true, { + "ignoreUnits": ["rpx"] + }] + "property-no-unknown": [ + true, + { + "ignoreProperties": [] + } + ], + "at-rule-no-unknown": [ + true, + { + "ignoreAtRules": ["apply", "use", "forward"] + } + ] + } +} +``` + +#### 配置忽略文件 + +根目录创建 .stylelintignore 文件,配置忽略文件如下: + +```basic +*.min.js +dist +public +node_modules +``` + +#### 添加 Stylelint 脚本 + +package.json 添加 Stylelint 检测指令: + +```json + "scripts": { + "lint:stylelint": "stylelint \"**/*.{css,scss,vue,html}\" --fix" + } +``` + +#### 保存自动修复 + +项目根目录下`.vscode/settings.json` 文件添加配置: + +```json +{ + "editor.codeActionsOnSave": { + "source.fixAll.stylelint": true + }, + "stylelint.validate": ["css", "scss", "vue", "html"] +} +``` + +为了验证把尺寸属性 width 放置在定位属性 position 前面,根据 [CSS 书写顺序规范](https://jingyan.baidu.com/article/647f0115cf48957f2148a8a3.html) 推断是不符合规范的,在保存时 Stylelint 自动将属性重新排序,达到预期。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/PixPin_2024-09-18_20-39-04.gif) + +#### 测试 + +执行以下命令进行检测 + +```bash +npm run lint:stylelint +``` + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240918203537608.png) + +## Git提交规范配置 + + + +配置 Husky 的 `pre-commit` 和 `commit-msg` 钩子,实现代码提交的自动化检查和规范化。 + +- **pre-commit**: 使用 Husky + Lint-staged,在提交前进行代码规范检测和格式化。确保项目已配置 ESLint、Prettier 和 Stylelint。 +- **commit-msg**: 结合 Husky、Commitlint、Commitizen 和 cz-git,生成规范化且自定义的 Git commit 信息。 + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240919150854229.png) + + + +### 集成 Husky + +Husky 是 Git 钩子工具,可以设置在 git 各个阶段(`pre-commit`、`commit-msg` 等)触发。 + +**Husky官网**:[https://typicode.github.io](https://typicode.github.io/husky/zh/get-started.html) + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240919094059492.png) + +#### 安装依赖 + +```bash +pnpm add -D husky +``` + +#### 初始化 + +`init` 命令简化了项目中的 husky 设置。它会在 `.husky/` 中创建 `pre-commit` 脚本,并更新 `package.json` 中的 `prepare` 脚本。 + +```bash +pnpm exec husky init +``` + +#### 测试 + +![image-20240919100633078](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240919100633078.png) + +通过 `pre-commit` 钩子,可以自动运行各种代码检查工具,在提交代码前强制执行代码质量和样式检查。常见的工具包括: + +- **`eslint`**:用于检查和修复 JavaScript/TypeScript 代码中的问题。 +- **`stylelint`**:用于检测和修复 CSS/SCSS 样式问题。 + +接下来,集成 **`lint-staged`** 和 **`commitlint`** 来进一步完善开发体验。 + +### 集成 lint-staged + +`lint-staged` 是一个工具,专门用于只对 Git 暂存区的文件运行 lint 或其他任务,确保只检查和修复被修改或新增的代码部分,而不会影响整个代码库。这样可以显著提升效率,尤其是对于大型项目。 + +#### 安装依赖 + +使用以下命令安装 `lint-staged`: + +```json +pnpm add -D lint-staged +``` + +#### 配置 lint-staged + +在 `package.json` 中添加 `lint-staged` 配置,确保在 `pre-commit` 阶段自动检测暂存的文件: + +```json +{ + "name": "vue-uniapp-template", + "version": "0.0.0", + "lint-staged": { + "*.{js,ts}": [ + "eslint --fix", + "prettier --write" + ], + "*.{cjs,json}": [ + "prettier --write" + ], + "*.{vue,html}": [ + "eslint --fix", + "prettier --write", + "stylelint --fix" + ], + "*.{scss,css}": [ + "stylelint --fix", + "prettier --write" + ], + "*.md": [ + "prettier --write" + ] + } +} +``` + +在 `package.json` 的 `scripts` 部分中,添加用于运行 `lint-staged` 的命令: + +```json +"scripts": { + "lint:lint-staged": "lint-staged" +} +``` + +#### 添加 Husky 钩子 + +在项目根目录的 `.husky/pre-commit` 中添加以下命令,确保在提交代码前执行 `lint-staged`: + +```bash +pnpm run lint:lint-staged +``` + +#### 测试 + +提交代码时,`lint-staged` 会自动对暂存的文件运行相应的 lint 任务。 + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240919112617112.png) + +通过这种集成方式,确保代码在提交前经过自动格式化和校验,提高代码质量和一致性。 + +### 集成 Commitlint + +`commitlint` 用于检查 Git 提交信息是否符合特定规范(如 Angular 提交规范),从而保证提交信息的一致性。 + +**Commitlint官网**:[https://commitlint.js.org/](https://commitlint.js.org/) + +#### 安装依赖 + +```bash +pnpm add -D @commitlint/cli @commitlint/config-conventional +``` + +#### 配置 Commitlint + +在项目根目录下创建 `commitlint.config.cjs` 文件,添加以下内容来启用 Angular 规范: + +```json +module.exports = { + // 继承的规则 + extends: ["@commitlint/config-conventional"], + // 自定义规则 + rules: { + // 提交类型枚举,git提交type必须是以下类型 @see https://commitlint.js.org/#/reference-rules + "type-enum": [ + 2, + "always", + [ + "feat", // 新增功能 + "fix", // 修复缺陷 + "docs", // 文档变更 + "style", // 代码格式(不影响功能,例如空格、分号等格式修正) + "refactor", // 代码重构(不包括 bug 修复、功能新增) + "perf", // 性能优化 + "test", // 添加疏漏测试或已有测试改动 + "build", // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等) + "ci", // 修改 CI 配置、脚本 + "revert", // 回滚 commit + "chore", // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例) + ], + ], + "subject-case": [0], // subject大小写不做校验 + }, +}; +``` + +#### 添加 Husky 钩子 + +将 `commitlint` 与 Husky 集成,在 `.husky/commit-msg` 文件中添加以下内容,确保提交信息符合规范: + +```bash +npx --no-install commitlint --edit $1 +``` + +#### 测试 + +根据 Angular 的提交规范,提交信息由以下部分组成: + +1. **类型**:表示本次提交的类型,例如 `feat` (新功能)、`fix` (修复 bug)、`docs` (文档更新)。 +2. **作用域**(可选):说明本次提交影响的模块,例如 `auth`、`ui`。 +3. **简短描述**:简洁明了的提交描述,限定在 50 字符以内。 + +当你尝试提交不符合规范的提交信息时,提交会被阻止,并显示相关错误提示。如下图所示: + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240919134921181.png) + +### 集成 Commitizen 和 cz-git + +- **commitizen**: 是一个帮助开发者以标准化格式生成提交信息的工具。--[Commitizen 官方文档](https://commitizen.github.io/cz-cli/) + +- **cz-git**: `cz-git` 是 `Commitizen` 的适配器之一,它基于 `Commitizen`,提供了更多自定义功能和增强的交互体验。--[cz-git 官方文档](https://cz-git.qbb.sh/zh/) + +#### 安装依赖 + +```bash +pnpm add -D commitizen cz-git +``` + +#### 配置 cz-git + +在项目中初始化 `Commitizen`,并配置使用 `cz-git` 作为适配器。在 `package.json` 中添加以下配置: + +```json +"config": { + "commitizen": { + "path": "node_modules/cz-git" + } +} +``` + +在`commitlint` 的配置文件 `commitlint.config.cjs` 中添加配置,commitlint 配置模板:[https://cz-git.qbb.sh/zh/config/](https://cz-git.qbb.sh/zh/config/) + +```javascript +module.exports = { + // 继承的规则 + extends: ["@commitlint/config-conventional"], + // 自定义规则 + rules: { + // ... + }, + // cz-git 配置 + prompt: { + messages: { + type: "选择你要提交的类型 :", + scope: "选择一个提交范围(可选):", + customScope: "请输入自定义的提交范围 :", + subject: "填写简短精炼的变更描述 :\n", + body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n', + breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n', + footerPrefixesSelect: "选择关联issue前缀(可选):", + customFooterPrefix: "输入自定义issue前缀 :", + footer: "列举关联issue (可选) 例如: #31, #I3244 :\n", + generatingByAI: "正在通过 AI 生成你的提交简短描述...", + generatedSelectByAI: "选择一个 AI 生成的简短描述:", + confirmCommit: "是否提交或修改commit ?", + }, + // prettier-ignore + types: [ + { value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" }, + { value: "fix", name: "修复: 🐛 修复缺陷", emoji: ":bug:" }, + { value: "docs", name: "文档: 📝 文档变更", emoji: ":memo:" }, + { value: "style", name: "格式: 💄 代码格式(不影响功能,例如空格、分号等格式修正)", emoji: ":lipstick:" }, + { value: "refactor", name: "重构: ♻️ 代码重构(不包括 bug 修复、功能新增)", emoji: ":recycle:" }, + { value: "perf", name: "性能: ⚡️ 性能优化", emoji: ":zap:" }, + { value: "test", name: "测试: ✅ 添加疏漏测试或已有测试改动", emoji: ":white_check_mark:"}, + { value: "build", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)", emoji: ":package:"}, + { value: "ci", name: "集成: 🎡 修改 CI 配置、脚本", emoji: ":ferris_wheel:"}, + { value: "revert", name: "回退: ⏪️ 回滚 commit",emoji: ":rewind:"}, + { value: "chore", name: "其他: 🔨 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: ":hammer:"}, + ], + useEmoji: true, + emojiAlign: "center", + useAI: false, + aiNumber: 1, + themeColorCode: "", + scopes: [], + allowCustomScopes: true, + allowEmptyScopes: true, + customScopesAlign: "bottom", + customScopesAlias: "custom", + emptyScopesAlias: "empty", + upperCaseSubject: false, + markBreakingChangeMode: false, + allowBreakingChanges: ["feat", "fix"], + breaklineNumber: 100, + breaklineChar: "|", + skipQuestions: [], + issuePrefixes: [{ value: "closed", name: "closed: ISSUES has been processed" }], + customIssuePrefixAlign: "top", + emptyIssuePrefixAlias: "skip", + customIssuePrefixAlias: "custom", + allowCustomIssuePrefix: true, + allowEmptyIssuePrefix: true, + confirmColorize: true, + maxHeaderLength: Infinity, + maxSubjectLength: Infinity, + minSubjectLength: 0, + scopeOverrides: undefined, + defaultBody: "", + defaultIssues: "", + defaultScope: "", + defaultSubject: "", + }, +}; + +``` + +#### 添加 cz-git 脚本 + +在`package.json` 文件中添加 `commit` 脚本命令 + +```json + "scripts": { + "commit": "git-cz" + } +``` + +#### 测试 + +执行 `pnpm run commit` 命令后,按照提示输入相关信息,最终生成符合规范的提交信息。 + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240919150854229.png) + +## 整合 Sass + +**Sass**是帮助开发者编写、管理和维护样式的强大工具,通过 ` +``` + +在 `pages.json` 文件中,声明登录页面的路由: + +```json +// src/pages.json +{ + "pages": [ + { + "path": "pages/login/index", + "style": { + "navigationBarTitleText": "登录" + } + } + ] +} +``` + + + +### 登录测试 + +访问登录页面:http://localhost:5173/#/pages/login/index,输入用户名和密码 (`admin`/`123456`) 测试登录接口,登录成功后可以看到返回的访问令牌。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/PixPin_2024-09-22_17-21-05.gif) + +### 整合源码 + +整合`HTTP请求`代码版本:[vue-uniapp-template#737f6a](hhttps://gitee.com/youlaiorg/vue-uniapp-template/commit/737f6a3ee5217dcad006fa06b6fae63374d9826b)。 + + + +## 整合 Pinia + +> Pinia 是 Vue 的状态管理库,专为跨组件或页面共享状态设计。 + +- **Pinia 官方文档**: https://pinia.vuejs.org/zh/getting-started.html + + ![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20240921102456261.png) + +### 安装依赖 + +首先,安装 `pinia` 依赖: + +```bash +pnpm add pinia +``` + +### 全局注册 + +在项目的 `src` 目录下创建 `store` 文件夹,并新建 `index.ts` 文件,初始化并注册 Pinia 实例。 + +```typescript +// src/store/index.ts +import type { App } from "vue"; +import { createPinia } from "pinia"; + +const store = createPinia(); +// 注册 Pinia +export function setupStore(app: App) { + app.use(store); // 全局注册 Pinia +} +``` + +接着,将 `store` 在项目入口文件 `main.ts` 中引入,并将其作为全局插件传递给应用: + +```typescript +// src/main.ts +import { createSSRApp } from "vue"; +import App from "./App.vue"; + +import { setupStore } from "@/store"; + +export function createApp() { + const app = createSSRApp(App); + // 全局注册 store + setupStore(app); + + return { + app, + }; +} + +``` + +接下来,我们通过 Pinia 管理登录状态和用户信息,并在多个页面共享状态。 + +### 用户信息接口 + +编写一个 API 来获取当前登录用户的信息: + +```typescript +import request from "@/utils/request"; + +const USER_BASE_URL = "/api/v1/users"; + +const UserAPI = { + /** + * 获取当前登录用户信息 + * + * @returns 登录用户昵称、头像信息,包括角色和权限 + */ + getUserInfo(): Promise { + return request({ + url: `${USER_BASE_URL}/me`, + method: "GET", + }); + }, +}; +export default UserAPI; + +/** 登录用户信息 */ +export interface UserInfo { + /** 用户ID */ + userId?: number; + + /** 用户名 */ + username?: string; + + /** 昵称 */ + nickname?: string; + + /** 头像URL */ + avatar?: string; + + /** 角色 */ + roles: string[]; + + /** 权限 */ + perms: string[]; +} +``` + + + +### 用户状态管理 + +通过 Pinia 定义 `user` 模块,管理登录状态、用户信息等。 + +```typescript +// src/store/module/user.ts +import { defineStore } from "pinia"; +import AuthAPI from "@/api/auth"; +import UserAPI, { UserInfo } from "@/api/user"; + +export const useUserStore = defineStore("user", () => { + // 确保 token 是响应式的 + const token = ref(uni.getStorageSync("token") || ""); + const userInfo = ref(null); + + // 登录 + const login = async (username: string, password: string) => { + const { tokenType, accessToken } = await AuthAPI.login(username, password); + token.value = `${tokenType} ${accessToken}`; // Bearer token + uni.setStorageSync("token", token.value); + }; + + // 获取用户信息 + const getUserInfo = async () => { + const info = await UserAPI.getUserInfo(); + userInfo.value = info; + }; + + // 登出 + const logout = async () => { + await AuthAPI.logout(); + userInfo.value = null; + token.value = ""; // 清空 token + uni.removeStorageSync("token"); // 从本地缓存移除 token + }; + + return { + token, + userInfo, + login, + logout, + getUserInfo, + }; +}); +``` + +### 个人中心页面 + +个人中心页面展示用户的头像和昵称,未登录时引导用户去登录。 + +```vue + + + +``` + +登录页通过 `Pinia` 实现用户信息的全局状态管理,并在登录成功后跳转到个人中心页面。 + +```vue + + + + + +``` + + + +### 测试效果 + +登录后,个人中心会显示用户的头像和昵称。通过 Pinia 实现了登录状态的共享和跨页面传递。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/PixPin_2024-09-22_17-21-011.gif) + + + +### 整合源码 + +整合`Pinia`代码版本:[vue-uniapp-template#737f6a3](https://gitee.com/youlaiorg/vue-uniapp-template/commit/737f6a3ee5217dcad006fa06b6fae63374d9826b)。 + +## 反向代理 + +在开发中,若服务端没有启用 CORS(跨域资源共享),浏览器会基于安全策略拦截跨域请求,导致无法访问接口。为了绕过这个问题,我们可以通过 Vite 的反向代理功能,将开发阶段的请求代理到真实的 API 服务器上,伪装成同源请求。 + +本节将介绍如何配置 Vite 的反向代理来处理跨域请求。 + +--- + +### 环境变量配置 + +我们将通过环境变量来管理项目端口和 API 请求地址,以下是 `.env.development` 中的相关配置: + +```bash +# .env.development + +# 项目运行的端口号 +VITE_APP_PORT=5173 + +# API 请求的基础路径(开发环境) +VITE_APP_BASE_API=/dev-api + +# 真实 API 服务器的 URL +VITE_APP_API_URL=https://api.youlai.tech +``` + +### 请求工具的调整 + +为了让请求走代理,我们需要在请求工具中将 `VITE_APP_API_URL` 替换为 `VITE_APP_BASE_API`。这样,所有对 `API` 的请求都会通过代理标识 `/dev-api` 进行转发。 + +```typescript +export default function request(options: UniApp.RequestOptions): Promise { + return new Promise((resolve, reject) => { + uni.request({ + ...options, + // 原请求方式: 使用真实 API URL + // url: `${import.meta.env.VITE_APP_API_URL}${options.url}`, // 示例: https://api.youlai.tech/login + + // 修改后:使用代理标识,实际转发到真实 API + url: `${import.meta.env.VITE_APP_BASE_API}${options.url}`, // 示例: http://localhost:5173/dev-api/login + }); + }); +} +``` + +### Vite 反向代理配置 + +接下来,在 `vite.config.ts` 中添加反向代理配置,将 `/dev-api` 的请求代理到 `VITE_APP_API_URL`,通过 `http-proxy` 实现请求的转发。 + +```typescript +// vite.config.ts +import { defineConfig, UserConfig, ConfigEnv, loadEnv } from "vite"; + +export default defineConfig(async ({ mode }: ConfigEnv): Promise => { + const env = loadEnv(mode, process.cwd()); + + return { + server: { + host: "0.0.0.0", + port: +env.VITE_APP_PORT, + open: true, + // 反向代理配置 + proxy: { + [env.VITE_APP_BASE_API]: { + target: env.VITE_APP_API_URL, // 目标服务器 + changeOrigin: true, // 支持跨域 + rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), // 去掉前缀 + }, + }, + }, + plugins: [ + // 插件配置... + ], + }; +}); +``` + +### 测试与验证 + +在配置好反向代理后,浏览器发出的请求将被 Vite 的代理服务器拦截并转发至真实的 API 地址。例如,浏览器请求 `http://localhost:5173/dev-api/api/v1/auth/login` 时,Vite 会将该请求代理到 `https://api.youlai.tech/api/v1/auth/login`。 + +下图展示了这一过程,浏览器认为请求的 URL 与应用的主机地址一致,因此不会阻止该请求,即便真实请求已通过代理转发到外部服务器。 + +![反向代理跨域](https://raw.gitmirror.com/youlaitech/images/main/docs/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E8%B7%A8%E5%9F%9F.gif) + +需要注意,反向代理的目标是伪装请求来源,虽然它绕过了浏览器的同源策略,但有时也会让开发者误以为请求地址错误。实际上,这是由于代理转发过程造成的表面请求地址与真实请求地址的差异。 + +### 整合源码 + +整合`反向代理`和`环境变量`代码版本:[vue-uniapp-template#272d643](https://gitee.com/youlaiorg/vue-uniapp-template/tree/272d643744b3b009a90a54e9344a0fc827f8b9fc)。 + +## 整合 `wot-design-uni` + +`wot-design-uni` 是基于 `Vue 3` 和 `TypeScript` 构建的高质量组件库。组件库遵循 `Wot Design` 的设计规范,提供 70 多个组件,支持暗黑模式、国际化和自定义主题,旨在为开发者提供一致的 UI 交互,同时提高开发效率。 + +> **说明:** 本文档整合步骤基于 `wot-design-uni` 官方文档编写,建议开发者参考 [官方文档](https://wot-design-uni.pages.dev/guide/quick-use.html#npm-%E5%AE%89%E8%A3%85) 进行安装和配置,以确保组件库的正确使用。 + +--- + +### 安装依赖 + +根据官方文档,使用 `pnpm` 安装组件库的依赖: + +```bash +pnpm add wot-design-uni +``` + +### 配置自动引入组件 + +在传统的 `Vue` 项目中,使用组件需要手动安装、引用、注册。而使用 `easycom` 可以简化这些操作。只要组件路径符合规范,就可以直接在页面中使用,无需手动导入和注册。 + +在 `pages.json` 文件中配置 `easycom` 自动引入: + +```json +// pages.json +{ + "easycom": { + "autoscan": true, + "custom": { + "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue" + } + }, + "pages": [ + // 这里是项目已有的内容 + ] +} +``` + +**关于 `easycom`:** +`easycom` 是 `uni-app` 提供的自动化引入功能,更多细节请参考 [easycom 官方文档](https://uniapp.dcloud.net.cn/collocation/pages.html#easycom)。 + +### Volar 支持 + +为了让 `Volar` 正确识别和提示全局组件,你需要在项目的 `tsconfig.json` 文件中配置全局组件类型支持: + +```json +// tsconfig.json +{ + "compilerOptions": { + "types": ["wot-design-uni/global"] + } +} +``` + +这将确保你在 `TypeScript` 项目中编写代码时,Volar 能提供完整的类型支持和代码提示。 + +### 测试组件 + +安装和配置完成后,你可以开始使用 `wot-design-uni` 的组件。在页面中,直接写组件标签即可,无需手动导入和注册: + +```vue + +``` + +你将看到如下按钮效果: + +![](https://raw.gitmirror.com/youlaitech/images/main/docs/image-20240925122808028.png) + +### 整合源码 + +整合`wot-design-uni` 代码版本:[vue-uniapp-template#a775721](https://gitee.com/youlaiorg/vue-uniapp-template/tree/a7757213f282a6414a3cf2cc13aa2c54a9479ae4)。 + + + +## 项目部署 + + + +### H5 部署 + +执行 `pnpm run build:h5` 命令来完成项目的打包: + +```bash +pnpm run build:h5 +``` + +打包后生成的静态文件位于 `dist/build/h5` 目录下。将该目录下的文件复制到服务器的 `/usr/share/nginx/html/vue-uniapp-template` 目录。 + +接下来,配置 nginx: + +```nginx +# nginx.conf +server { + listen 80; + server_name localhost; + location / { + root /usr/share/nginx/html/vue-uniapp-template; + index index.html index.htm; + } + # 反向代理配置 + location /prod-api/ { + # 将 api.youlai.tech 替换为后端 API 地址,注意保留后面的斜杠 / + proxy_pass http://api.youlai.tech/; + } +} +``` + +这样配置完成后,就可以通过 `nginx `服务器来访问你的项目了。 + +### 小程序发布 + +### 下载工具 + +下载 [ HBuilder X ](https://www.dcloud.io/hbuilderx.html) 编辑器 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024232438606.png) + + + +下载 [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html) + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024232328575.png) + +### 获取小程序 AppID + +访问 [微信公众平台](https://mp.weixin.qq.com/)申请小程序,获取 `AppID`。如果已申请,可在 `首页` → `小程序信息` → `查看详情` 查看 AppID + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024231827075.png) + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024232218176.png) + +### 配置项目 + +使用 HBuilder X 打开项目,修改 `manifest.json` 文件中的小程序配置,并填写获取的 AppID。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024232920676.png) + +### 设置微信开发者工具 + +使用微信扫码登录微信开发者工具,开启服务端口:点击工具栏`设置`→`安全设置`→`安全`→`服务端口`,选择打开。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024233058539.png) + +### 运行项目 + +在 HBuilder X 中,点击 `运行`→`运行到小程序模拟器`→`微信开发者工具`。 + +![image-20241024233326086](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024233326086.png) + +项目编译完成后,微信开发者工具会自动启动并呈现页面。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024233741449.png) + +### 上传发布 + +在微信开发者工具中,点击 `上传` 将应用发布到小程序平台。 + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024234052335.png) + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/image-20241024234313807.png) + +### 查看效果 + +最后,使用手机打开小程序查看效果: + +![](https://raw.gitmirror.com/youlaitech/image/main/blog/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20241024234523.jpg) diff --git a/docs/theme-guide.md b/docs/theme-guide.md new file mode 100644 index 0000000..652c502 --- /dev/null +++ b/docs/theme-guide.md @@ -0,0 +1,361 @@ +# 主题设置功能指南 + +## 功能概述 + +本项目提供了完整的主题设置功能,支持暗黑模式切换和主题色自定义,让用户可以个性化应用的外观。 + +## 功能特性 + +### 1. 暗黑模式 + +- 支持浅色/暗黑模式切换 +- 自动保存用户偏好设置 +- 平滑的过渡动画效果 +- 导航栏颜色自动适配 + +### 2. 主题色设置 + +- 12种预设主题色可选 +- 支持自定义十六进制颜色值 +- 实时预览效果 +- 主题色持久化存储 + +### 3. 效果预览 + +- 按钮样式预览 +- 文本颜色预览 +- 标签组件预览 +- 实时更新显示 + +## 文件结构 + +``` +src/ +├── composables/ +│ └── theme/ +│ ├── theme.ts # 主题管理核心逻辑 +│ └── rootTheme.ts # 主题配置和默认值 +├── store/ +│ └── modules/ +│ └── theme.ts # 主题状态管理 +├── pages/ +│ └── setting.vue # 主题设置页面 +├── utils/ +│ ├── theme.ts # 主题工具函数 +│ └── colorUtils.ts # 颜色处理工具 +└── styles/ + └── global.scss # 全局主题样式 +``` + +## 核心组件说明 + +### 1. useTheme Composable + +位置:`src/composables/theme/theme.ts` + +主要功能: + +- 主题模式切换(浅色/暗黑) +- 主题色设置和保存 +- 导航栏颜色适配 +- 本地存储管理 + +```typescript +const { + theme, // 当前主题模式 + themeVars, // 主题变量 + toggleTheme, // 切换主题模式 + setThemeColor, // 设置主题色 + initTheme, // 初始化主题 +} = useTheme(); +``` + +### 2. useThemeStore + +位置:`src/store/modules/theme.ts` + +主要功能: + +- 主题色状态管理 +- CSS变量动态设置 +- 多平台兼容处理 + +```typescript +const themeStore = useThemeStore(); + +// 设置主题色 +themeStore.setPrimaryColor("#165DFF"); + +// 初始化主题 +themeStore.initTheme(); +``` + +### 3. 主题设置页面 + +位置:`src/pages/setting.vue` + +功能模块: + +- 暗黑模式开关 +- 预设主题色选择 +- 自定义颜色输入 +- 效果实时预览 +- 重置默认主题 + +## 使用方法 + +### 1. 基础使用 + +在页面中引入主题功能: + +```vue + + + +``` + +### 2. 在布局中使用 + +在 `src/layouts/tabbar.vue` 中已经集成了主题功能: + +```vue + +``` + +### 3. 自定义主题色 + +支持的颜色格式: + +- 6位十六进制:`#165DFF` +- 3位十六进制:`#16F`(会自动转换为6位) + +```javascript +// 设置自定义主题色 +const customColor = "#FF6B6B"; +themeStore.setPrimaryColor(customColor); +``` + +## 预设主题色 + +项目提供了12种预设主题色: + +| 颜色名称 | 颜色值 | 说明 | +| -------- | ------- | ---------- | +| 蓝色 | #0055FE | 默认主题色 | +| 红色 | #CD5C5C | 经典红色 | +| 绿色 | #228B22 | 自然绿色 | +| 紫色 | #722ED1 | 优雅紫色 | +| 橙色 | #FA8C16 | 活力橙色 | +| 黄色 | #FADB14 | 明亮黄色 | +| 青色 | #13C2C2 | 清新青色 | +| 粉色 | #EB2F96 | 温馨粉色 | +| 天蓝色 | #1890FF | 科技蓝色 | +| 深红色 | #F5222D | 警示红色 | + +## 样式变量 + +主题系统使用CSS变量来实现动态主题切换: + +```scss +:root { + /* 主色调 */ + --primary-color: #165dff; + --primary-color-light: #94bfff; + --primary-color-dark: #0e3c9b; + + /* 功能色 */ + --success-color: #0fc6c2; + --warning-color: #ff7d00; + --danger-color: #f5222d; + --info-color: #86909c; +} +``` + +在组件中使用: + +```scss +.my-button { + background-color: var(--primary-color); + color: #fff; + + &:hover { + background-color: var(--primary-color-dark); + } +} +``` + +## 平台兼容性 + +### H5平台 + +- 使用CSS变量动态设置主题 +- 支持所有现代浏览器 +- 平滑的过渡动画 + +### 小程序平台 + +- 使用原生API设置TabBar样式 +- 通过工具函数处理样式应用 +- 兼容微信、支付宝等小程序 + +### APP平台 + +- 支持原生导航栏颜色设置 +- 状态栏颜色自动适配 +- 性能优化处理 + +## 最佳实践 + +### 1. 主题初始化 + +在应用启动时初始化主题: + +```typescript +// src/App.vue +import { useThemeStore } from "@/store/modules/theme"; + +const themeStore = useThemeStore(); + +onLaunch(() => { + // 初始化主题 + themeStore.initTheme(); +}); +``` + +### 2. 组件中使用主题 + +```vue + + + +``` + +### 3. 暗黑模式适配 + +```scss +// 暗黑模式样式 +:deep(.wd-config-provider[data-theme="dark"]) { + .my-component { + background: #2a2a2a; + color: #fff; + + .title { + color: var(--primary-color); + } + } +} +``` + +## 注意事项 + +1. **性能优化**:主题切换时避免频繁的DOM操作 +2. **兼容性**:小程序环境下某些CSS特性可能不支持 +3. **用户体验**:提供平滑的过渡动画效果 +4. **持久化**:确保用户设置能够正确保存和恢复 +5. **响应式**:在不同屏幕尺寸下保持良好的显示效果 + +## 故障排除 + +### 1. 主题色不生效 + +- 检查CSS变量是否正确设置 +- 确认组件是否在 `wd-config-provider` 包裹内 +- 验证颜色值格式是否正确 + +### 2. 暗黑模式切换异常 + +- 检查 `theme` 状态是否正确更新 +- 确认暗黑模式样式是否正确编写 +- 验证导航栏颜色设置是否生效 + +### 3. 设置不持久化 + +- 检查本地存储权限 +- 确认存储key是否正确 +- 验证初始化逻辑是否执行 + +## 扩展开发 + +### 1. 添加新的预设主题色 + +在 `src/composables/theme/rootTheme.ts` 中添加: + +```typescript +export const colorColumns = [ + // 现有颜色... + { + value: "#YOUR_COLOR", + label: "你的颜色名称", + }, +]; +``` + +### 2. 自定义主题变量 + +在 `src/composables/theme/rootTheme.ts` 中扩展: + +```typescript +export const initThemeVars: ConfigProviderThemeVars = { + colorTheme: colorColumns[0].value, + // 添加更多主题变量 + colorSuccess: "#52c41a", + colorWarning: "#faad14", + colorDanger: "#ff4d4f", +}; +``` + +### 3. 添加主题切换动画 + +```scss +.theme-transition { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} +``` + +通过以上指南,你可以充分利用项目的主题设置功能,为用户提供个性化的应用体验。 diff --git a/docs/wechat-login-guide.md b/docs/wechat-login-guide.md new file mode 100644 index 0000000..9823d4d --- /dev/null +++ b/docs/wechat-login-guide.md @@ -0,0 +1,342 @@ +# 微信小程序手机授权登录功能指南 + +## 功能概述 + +本项目实现了完整的微信小程序手机授权登录功能,包括: + +- 微信登录授权 +- 手机号获取授权 +- 头像昵称填写(使用微信小程序新能力) +- 用户信息完善流程 +- 登录状态管理 + +## 功能特性 + +### 1. 微信登录流程 + +- **基础微信登录**:使用 `uni.login()` 获取 code,调用后端接口完成登录 +- **增强微信登录**:支持更多用户信息和手机号一次性授权 +- **登录状态检查**:自动检查用户信息完整性,引导用户完善信息 + +### 2. 手机号授权 + +- **一键授权**:使用微信小程序 `getPhoneNumber` 能力 +- **安全获取**:通过后端接口解密获取真实手机号 +- **状态显示**:显示脱敏手机号,支持重新授权 + +### 3. 头像昵称填写 + +- **微信新能力**:使用 `chooseAvatar` 和 `type="nickname"` 输入框 +- **自动上传**:头像选择后自动上传到服务器 +- **实时预览**:支持头像实时预览和昵称输入 + +### 4. 用户信息完善 + +- **智能引导**:登录后自动检查信息完整性 +- **分步填写**:头像、昵称、性别、手机号分步骤完善 +- **跳过机制**:允许用户暂时跳过,但会提示影响功能使用 + +## 文件结构 + +``` +src/ +├── pages/ +│ └── login/ +│ ├── index.vue # 登录页面 +│ └── complete-profile.vue # 完善信息页面 +├── components/ +│ └── WechatProfile.vue # 微信头像昵称组件 +├── api/ +│ ├── auth.ts # 认证API +│ ├── user.ts # 用户API +│ └── file.ts # 文件上传API +├── store/ +│ └── modules/ +│ └── user.ts # 用户状态管理 +└── utils/ + ├── auth.ts # 认证工具函数 + └── storage.ts # 存储工具函数 +``` + +## 核心组件说明 + +### 1. 登录页面 (`pages/login/index.vue`) + +**主要功能:** + +- 用户名密码登录 +- 微信一键登录 +- 登录状态检查和跳转 + +**关键代码:** + +```typescript +// 微信登录处理 +const handleWechatLogin = async () => { + const { code } = await uni.login({ provider: "weixin" }); + + // 尝试增强登录 + try { + const result = await userStore.loginByWechatMini({ code }); + // 检查信息完整性 + if (result.isNewUser || !result.isProfileComplete) { + // 跳转到完善信息页面 + uni.navigateTo({ + url: `/pages/login/complete-profile?redirect=${redirect}`, + }); + } + } catch (error) { + // 回退到基础登录 + await userStore.loginByWechat(code); + } +}; +``` + +### 2. 完善信息页面 (`pages/login/complete-profile.vue`) + +**主要功能:** + +- 头像上传(支持微信新能力) +- 昵称输入 +- 性别选择 +- 手机号授权 +- 信息提交和验证 + +**关键代码:** + +```typescript +// 手机号授权 +const onGetPhoneNumber = async (e: any) => { + if (e.detail.errMsg === "getPhoneNumber:ok") { + const phoneData = await UserAPI.getPhoneNumber({ + code: e.detail.code, + encryptedData: e.detail.encryptedData, + iv: e.detail.iv, + }); + profileForm.mobile = phoneData.phoneNumber; + } +}; +``` + +### 3. 微信头像昵称组件 (`components/WechatProfile.vue`) + +**主要功能:** + +- 使用微信小程序头像选择能力 +- 昵称输入框(type="nickname") +- 性别选择 +- 数据双向绑定 + +**关键代码:** + +```vue + + + + + +``` + +## API 接口说明 + +### 1. 认证相关接口 + +```typescript +// 基础微信登录 +AuthAPI.wechatLogin(code: string): Promise + +// 增强微信登录 +AuthAPI.wechatMiniLogin(data: WechatMiniLoginData): Promise +``` + +### 2. 用户相关接口 + +```typescript +// 获取微信手机号 +UserAPI.getPhoneNumber(data: WechatPhoneData): Promise + +// 绑定手机号 +UserAPI.bindMobile(data: MobileBindingForm): Promise + +// 更新用户信息 +UserAPI.updateProfile(data: UserProfileForm): Promise +``` + +### 3. 文件上传接口 + +```typescript +// 上传文件 +FileAPI.upload(filePath: string): Promise +``` + +## 类型定义 + +### 微信登录相关 + +```typescript +interface WechatMiniLoginData { + code: string; + userInfo?: { + nickName?: string; + avatarUrl?: string; + gender?: number; + }; + phoneData?: { + code: string; + encryptedData?: string; + iv?: string; + }; +} + +interface WechatLoginResult extends LoginResult { + isNewUser?: boolean; + isProfileComplete?: boolean; + userInfo?: { + userId?: number; + username?: string; + nickname?: string; + avatar?: string; + mobile?: string; + }; +} +``` + +### 手机号授权相关 + +```typescript +interface WechatPhoneData { + code: string; + encryptedData?: string; + iv?: string; +} + +interface PhoneNumberResult { + phoneNumber: string; + purePhoneNumber?: string; + countryCode?: string; +} +``` + +## 使用流程 + +### 1. 用户首次登录 + +1. 用户点击微信登录按钮 +2. 调用 `uni.login()` 获取微信 code +3. 调用后端登录接口,获取 token +4. 检查用户信息完整性 +5. 如果信息不完整,跳转到完善信息页面 + +### 2. 完善用户信息 + +1. 用户进入完善信息页面 +2. 选择头像(使用微信新能力或传统上传) +3. 输入昵称(使用 type="nickname" 输入框) +4. 选择性别 +5. 授权获取手机号 +6. 提交信息,更新用户资料 + +### 3. 后续登录 + +1. 用户再次登录时,检查信息完整性 +2. 如果信息完整,直接跳转到主页 +3. 如果信息不完整,引导用户完善 + +## 配置要求 + +### 1. 微信小程序配置 + +在 `manifest.json` 中配置: + +```json +{ + "mp-weixin": { + "appid": "your-appid", + "setting": { + "urlCheck": false + }, + "permission": { + "scope.userInfo": { + "desc": "用于完善用户资料" + } + } + } +} +``` + +### 2. 后端接口要求 + +- 支持微信登录 code 解析 +- 支持微信手机号解密 +- 支持文件上传 +- 支持用户信息更新 + +## 注意事项 + +### 1. 微信小程序新能力 + +- `chooseAvatar` 和 `type="nickname"` 需要微信基础库 2.21.2+ +- 需要在微信开发者工具中测试 +- 真机调试时需要注意兼容性 + +### 2. 手机号授权 + +- 需要微信小程序认证 +- 需要在微信公众平台配置服务器域名 +- 手机号解密需要在后端完成 + +### 3. 用户体验 + +- 提供跳过机制,避免强制完善信息 +- 显示脱敏手机号,保护用户隐私 +- 支持重新授权和修改信息 + +## 扩展功能 + +### 1. 社交登录 + +可以扩展支持其他社交平台登录: + +- QQ 登录 +- 支付宝登录 +- 苹果登录 + +### 2. 实名认证 + +可以添加实名认证功能: + +- 身份证验证 +- 人脸识别 +- 银行卡验证 + +### 3. 多端同步 + +可以实现多端登录状态同步: + +- H5 端登录 +- APP 端登录 +- 小程序端登录 + +## 故障排除 + +### 1. 微信登录失败 + +- 检查 appid 配置 +- 检查服务器域名配置 +- 检查网络连接 + +### 2. 手机号授权失败 + +- 检查小程序是否已认证 +- 检查后端解密接口 +- 检查用户授权状态 + +### 3. 头像上传失败 + +- 检查文件上传接口 +- 检查文件大小限制 +- 检查网络状态 + +## 总结 diff --git a/package.json b/package.json index 2104be3..f9e5359 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,8 @@ "@dcloudio/uni-mp-weixin": "3.0.0-4020420240722002", "@dcloudio/uni-mp-xhs": "3.0.0-4020420240722002", "@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002", + "@uni-helper/uni-use": "^0.19.14", + "@vueuse/core": "10.11.1", "pinia": "^2.2.2", "vue": "^3.5.13", "wot-design-uni": "^1.4.0" @@ -103,6 +105,8 @@ "@dcloudio/uni-stacktracey": "3.0.0-4020420240722002", "@dcloudio/vite-plugin-uni": "3.0.0-4020420240722002", "@eslint/js": "^9.10.0", + "@uni-helper/vite-plugin-uni-layouts": "^0.1.10", + "@uni-helper/vite-plugin-uni-pages": "^0.2.28", "@vue/runtime-core": "^3.4.21", "commitizen": "^4.3.0", "cz-git": "^1.9.4", diff --git a/pages.config.ts b/pages.config.ts new file mode 100644 index 0000000..f6d4302 --- /dev/null +++ b/pages.config.ts @@ -0,0 +1,11 @@ +// pages.config.ts +import { defineUniPages } from "@uni-helper/vite-plugin-uni-pages"; + +export default defineUniPages({ + // 你也可以定义 pages 字段,它具有最高的优先级。 + pages: [], + globalStyle: { + navigationBarTextStyle: "black", + navigationBarTitleText: "@uni-helper", + }, +}); diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..13adfc6 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,126 @@ +import request from "@/utils/request"; + +const AuthAPI = { + /** + * 登录接口 + * + * @param username 用户名 + * @param password 密码 + * @returns 返回 token + */ + login(data: LoginFormData): Promise { + return request({ + url: "/api/v1/auth/login", + method: "POST", + data: data, + header: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + }, + + /** + * 微信登录接口 + * + * @param code 微信登录code + * @returns 返回 token + */ + wechatLogin(code: string): Promise { + return request({ + url: "/api/v1/auth/wechat-login", + method: "POST", + data: { code }, + header: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + }, + + /** + * 微信小程序登录接口(增强版) + * + * @param data 微信登录数据 + * @returns 返回 token 和用户信息 + */ + wechatMiniLogin(data: WechatMiniLoginData): Promise { + return request({ + url: "/api/v1/auth/wechat-mini-login", + method: "POST", + data: data, + header: { + "Content-Type": "application/json", + }, + }); + }, + + /** + * 登出接口 + */ + logout(): Promise { + return request({ + url: "/api/v1/auth/logout", + method: "DELETE", + }); + }, +}; + +export default AuthAPI; + +/** 登录响应 */ +export interface LoginResult { + /** 访问token */ + accessToken: string; + /** token 类型 */ + tokenType?: string; +} + +export interface LoginFormData { + username: string; + password: string; +} + +/** 微信小程序登录数据 */ +export interface WechatMiniLoginData { + /** 微信登录code */ + code: string; + /** 用户信息(可选) */ + userInfo?: { + /** 昵称 */ + nickName?: string; + /** 头像URL */ + avatarUrl?: string; + /** 性别 */ + gender?: number; + /** 国家 */ + country?: string; + /** 省份 */ + province?: string; + /** 城市 */ + city?: string; + }; + /** 手机号授权数据(可选) */ + phoneData?: { + /** 手机号授权code */ + code: string; + /** 加密数据 */ + encryptedData?: string; + /** 初始向量 */ + iv?: string; + }; +} + +/** 微信登录结果 */ +export interface WechatLoginResult extends LoginResult { + /** 是否为新用户 */ + isNewUser?: boolean; + /** 用户信息是否完整 */ + isProfileComplete?: boolean; + /** 用户基本信息 */ + userInfo?: { + userId?: number; + username?: string; + nickname?: string; + avatar?: string; + mobile?: string; + }; +} diff --git a/src/api/auth/index.ts b/src/api/auth/index.ts deleted file mode 100644 index 7f8ae3a..0000000 --- a/src/api/auth/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import request from "@/utils/request"; - -const AuthAPI = { - /** - * 登录接口 - * - * @param username 用户名 - * @param password 密码 - * @returns 返回 token - */ - login(data: LoginFormData): Promise { - console.log("data", data); - return request({ - url: "/api/v1/auth/login", - method: "POST", - data: data, - header: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - }, - - /** - * 微信登录接口 - * - * @param code 微信登录code - * @returns 返回 token - */ - wechatLogin(code: string): Promise { - return request({ - url: "/api/v1/auth/wechat-login", - method: "POST", - data: { code }, - header: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); - }, - - /** - * 登出接口 - */ - logout(): Promise { - return request({ - url: "/api/v1/auth/logout", - method: "DELETE", - }); - }, -}; - -export default AuthAPI; - -/** 登录响应 */ -export interface LoginResult { - /** 访问token */ - accessToken: string; - /** token 类型 */ - tokenType?: string; -} - -export interface LoginFormData { - username: string; - password: string; -} diff --git a/src/api/file/index.ts b/src/api/file.ts similarity index 97% rename from src/api/file/index.ts rename to src/api/file.ts index 1e19bf9..c472c5c 100644 --- a/src/api/file/index.ts +++ b/src/api/file.ts @@ -1,4 +1,4 @@ -import { getToken } from "@/utils/cache"; +import { getToken } from "@/utils/storage"; import { ResultCodeEnum } from "@/enums/ResultCodeEnum"; // H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径 diff --git a/src/api/system/config.ts b/src/api/system/config.ts deleted file mode 100644 index fb42cde..0000000 --- a/src/api/system/config.ts +++ /dev/null @@ -1,104 +0,0 @@ -import request from "@/utils/request"; - -const CONFIG_BASE_URL = "/api/v1/config"; - -const ConfigAPI = { - /** 获取系统配置分页数据 */ - getPage(queryParams: ConfigPageQuery) { - return request>({ - url: `${CONFIG_BASE_URL}/page`, - method: "GET", - data: queryParams, - }); - }, - /** - * 获取系统配置表单数据 - * - * @param id ConfigID - * @returns Config表单数据 - */ - getFormData(id: number) { - return request({ - url: `${CONFIG_BASE_URL}/${id}/form`, - method: "GET", - }); - }, - - /** 添加系统配置*/ - add(data: ConfigForm) { - return request({ - url: `${CONFIG_BASE_URL}`, - method: "POST", - data: data, - }); - }, - - /** - * 更新系统配置 - * - * @param id ConfigID - * @param data Config表单数据 - */ - update(id: number, data: ConfigForm) { - return request({ - url: `${CONFIG_BASE_URL}/${id}`, - method: "PUT", - data: data, - }); - }, - - /** - * 删除系统配置 - * - * @param ids 系统配置ID - */ - deleteById(id: number) { - return request({ - url: `${CONFIG_BASE_URL}/${id}`, - method: "DELETE", - }); - }, - - refreshCache() { - return request({ - url: `${CONFIG_BASE_URL}/refresh`, - method: "PUT", - }); - }, -}; - -export default ConfigAPI; - -/** $系统配置分页查询参数 */ -export interface ConfigPageQuery extends PageQuery { - /** 搜索关键字 */ - keywords?: string; -} - -/** 系统配置表单对象 */ -export interface ConfigForm { - /** 主键 */ - id?: number; - /** 配置名称 */ - configName?: string; - /** 配置键 */ - configKey?: string; - /** 配置值 */ - configValue?: string; - /** 描述、备注 */ - remark?: string; -} - -/** 系统配置分页对象 */ -export interface ConfigPageVO { - /** 主键 */ - id?: number; - /** 配置名称 */ - configName?: string; - /** 配置键 */ - configKey?: string; - /** 配置值 */ - configValue?: string; - /** 描述、备注 */ - remark?: string; -} diff --git a/src/api/system/dept.ts b/src/api/system/dept.ts deleted file mode 100644 index cdbd165..0000000 --- a/src/api/system/dept.ts +++ /dev/null @@ -1,130 +0,0 @@ -import request from "@/utils/request"; - -const DEPT_BASE_URL = "/api/v1/dept"; - -const DeptAPI = { - /** - * 获取部门列表 - * - * @param queryParams 查询参数(可选) - * @returns 部门树形表格数据 - */ - getList(queryParams?: DeptQuery) { - return request({ - url: `${DEPT_BASE_URL}`, - method: "GET", - data: queryParams, - }); - }, - - /** 获取部门下拉列表 */ - getOptions() { - return request({ - url: `${DEPT_BASE_URL}/options`, - method: "GET", - }); - }, - - /** - * 获取部门表单数据 - * - * @param id 部门ID - * @returns 部门表单数据 - */ - getFormData(id: number) { - return request({ - url: `${DEPT_BASE_URL}/${id}/form`, - method: "GET", - }); - }, - - /** - * 新增部门 - * - * @param data 部门表单数据 - * @returns 请求结果 - */ - add(data: DeptForm) { - return request({ - url: `${DEPT_BASE_URL}`, - method: "POST", - data: data, - }); - }, - - /** - * 修改部门 - * - * @param id 部门ID - * @param data 部门表单数据 - * @returns 请求结果 - */ - update(id: number, data: DeptForm) { - return request({ - url: `${DEPT_BASE_URL}/${id}`, - method: "PUT", - data: data, - }); - }, - - /** - * 删除部门 - * - * @param ids 部门ID,多个以英文逗号(,)分隔 - * @returns 请求结果 - */ - deleteByIds(ids: string) { - return request({ - url: `${DEPT_BASE_URL}/${ids}`, - method: "DELETE", - }); - }, -}; - -export default DeptAPI; - -/** 部门查询参数 */ -export interface DeptQuery { - /** 搜索关键字 */ - keywords?: string; - /** 状态 */ - status?: number; -} - -/** 部门类型 */ -export interface DeptVO { - /** 子部门 */ - children?: DeptVO[]; - /** 创建时间 */ - createTime?: Date; - /** 部门ID */ - id?: number; - /** 部门名称 */ - name?: string; - /** 部门编号 */ - code?: string; - /** 父部门ID */ - parentId?: number; - /** 排序 */ - sort?: number; - /** 状态(1:启用;0:禁用) */ - status?: number; - /** 修改时间 */ - updateTime?: Date; -} - -/** 部门表单类型 */ -export interface DeptForm { - /** 部门ID(新增不填) */ - id?: number; - /** 部门名称 */ - name?: string; - /** 部门编号 */ - code?: string; - /** 父部门ID */ - parentId: number; - /** 排序 */ - sort?: number; - /** 状态(1:启用;0:禁用) */ - status?: number; -} diff --git a/src/api/system/dict.ts b/src/api/system/dict.ts deleted file mode 100644 index 39f08d3..0000000 --- a/src/api/system/dict.ts +++ /dev/null @@ -1,305 +0,0 @@ -import request from "@/utils/request"; - -const DICT_BASE_URL = "/api/v1/dicts"; - -const DictAPI = { - //--------------------------------------------------- - // 字典相关接口 - //--------------------------------------------------- - - /** - * 字典分页列表 - * - * @param queryParams 查询参数 - * @returns 字典分页结果 - */ - getPage(queryParams: DictPageQuery) { - return request>({ - url: `${DICT_BASE_URL}/page`, - method: "GET", - data: queryParams, - }); - }, - - /** - * 字典表单数据 - * - * @param id 字典ID - * @returns 字典表单数据 - */ - getFormData(id: number) { - return request({ - url: `${DICT_BASE_URL}/${id}/form`, - method: "GET", - }); - }, - - /** - * 新增字典 - * - * @param data 字典表单数据 - */ - create(data: DictForm) { - return request({ - url: `${DICT_BASE_URL}`, - method: "POST", - data: data, - }); - }, - - /** - * 修改字典 - * - * @param id 字典ID - * @param data 字典表单数据 - */ - update(id: number, data: DictForm) { - return request({ - url: `${DICT_BASE_URL}/${id}`, - method: "PUT", - data: data, - }); - }, - - /** - * 删除字典 - * - * @param ids 字典ID,多个以英文逗号(,)分隔 - */ - deleteByIds(ids: string) { - return request({ - url: `${DICT_BASE_URL}/${ids}`, - method: "DELETE", - }); - }, - - //--------------------------------------------------- - // 字典项相关接口 - //--------------------------------------------------- - /** - * 获取字典分页列表 - * - * @param queryParams 查询参数 - * @returns 字典分页结果 - */ - getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) { - return request>({ - url: `${DICT_BASE_URL}/${dictCode}/items/page`, - method: "GET", - data: queryParams, - }); - }, - - /** - * 获取字典项列表 - */ - getDictItems(dictCode: string) { - return request({ - url: `${DICT_BASE_URL}/${dictCode}/items`, - method: "GET", - }); - }, - - /** - * 新增字典项 - */ - createDictItem(dictCode: string, data: DictItemForm) { - return request({ - url: `${DICT_BASE_URL}/${dictCode}/items`, - method: "POST", - data: data, - }); - }, - - /** - * 获取字典项表单数据 - * - * @param id 字典项ID - * @returns 字典项表单数据 - */ - getDictItemFormData(dictCode: string, id: number) { - return request({ - url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`, - method: "GET", - }); - }, - - /** - * 修改字典项 - */ - updateDictItem(dictCode: string, id: number, data: DictItemForm) { - return request({ - url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, - method: "PUT", - data: data, - }); - }, - - /** - * 删除字典项 - */ - deleteDictItems(dictCode: string, ids: string) { - return request({ - url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, - method: "DELETE", - }); - }, -}; - -export default DictAPI; - -/** - * 字典查询参数 - */ -export interface DictPageQuery extends PageQuery { - /** - * 关键字(字典名称/编码) - */ - keywords?: string; - - /** - * 字典状态(1:启用,0:禁用) - */ - status?: number; -} - -/** - * 字典分页对象 - */ -export interface DictPageVO { - /** - * 字典ID - */ - id: number; - /** - * 字典名称 - */ - name: string; - /** - * 字典编码 - */ - dictCode: string; - /** - * 字典状态(1:启用,0:禁用) - */ - status: number; -} - -/** - * 字典 - */ -export interface DictForm { - /** - * 字典ID - */ - id?: number; - /** - * 字典名称 - */ - name?: string; - /** - * 字典编码 - */ - dictCode?: string; - /** - * 字典状态(1-启用,0-禁用) - */ - status?: number; - /** - * 备注 - */ - remark?: string; -} - -/** - * 字典查询参数 - */ -export interface DictItemPageQuery extends PageQuery { - /** 关键字(字典数据值/标签) */ - keywords?: string; - - /** 字典编码 */ - dictCode?: string; -} - -/** - * 字典分页对象 - */ -export interface DictItemPageVO { - /** - * 字典ID - */ - id: number; - /** - * 字典编码 - */ - dictCode: string; - /** - * 字典数据值 - */ - value: string; - /** - * 字典数据标签 - */ - label: string; - /** - * 状态(1:启用,0:禁用) - */ - status: number; - /** - * 字典排序 - */ - sort?: number; -} - -/** - * 字典 - */ -export interface DictItemForm { - /** - * 字典ID - */ - id?: number; - /** - * 字典编码 - */ - dictCode?: string; - /** - * 字典数据值 - */ - value?: string; - /** - * 字典数据标签 - */ - label?: string; - /** - * 状态(1:启用,0:禁用) - */ - status?: number; - /** - * 字典排序 - */ - sort?: number; - - /** - * 标签类型 - */ - tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined; -} - -/** - * 字典项下拉选项 - */ -export interface DictItemOption { - /** 字典数据值 */ - value: string | number; - - /** 字典数据标签 */ - label: string; - - /** 标签类型 */ - tagType?: "" | "success" | "info" | "warning" | "danger" | "primary"; - - /** 允许其他属性 */ - [key: string]: any; -} diff --git a/src/api/system/log.ts b/src/api/system/log.ts deleted file mode 100644 index 76f87f8..0000000 --- a/src/api/system/log.ts +++ /dev/null @@ -1,123 +0,0 @@ -import request from "@/utils/request"; - -const LOG_BASE_URL = "/api/v1/logs"; - -const LogAPI = { - /** - * 获取日志分页列表 - * - * @param queryParams 查询参数 - */ - getPage(queryParams: LogPageQuery) { - return request>({ - url: `${LOG_BASE_URL}/page`, - method: "GET", - data: queryParams, - }); - }, - - /** - * 获取访问趋势 - * - * @param queryParams - * @returns - */ - getVisitTrend(queryParams: VisitTrendQuery) { - return request({ - url: `${LOG_BASE_URL}/visit-trend`, - method: "GET", - data: queryParams, - }); - }, - - /** - * 获取访问趋势 - * - * @param queryParams - * @returns - */ - getVisitStats() { - return request({ - url: `${LOG_BASE_URL}/visit-stats`, - method: "GET", - }); - }, -}; - -export default LogAPI; - -/** - * 日志分页查询对象 - */ -export interface LogPageQuery extends PageQuery { - /** 搜索关键字 */ - keywords?: string; - /** 操作时间 */ - createTime?: [string, string] | string; -} - -/** - * 系统日志分页VO - */ -export interface LogVO { - /** 主键 */ - id?: number; - /** 日志模块 */ - module?: string; - /** 日志内容 */ - content?: string; - /** 请求路径 */ - requestUri?: string; - /** 请求方法 */ - method?: string; - /** IP 地址 */ - ip?: string; - /** 地区 */ - region?: string; - /** 浏览器 */ - browser?: string; - /** 终端系统 */ - os?: string; - /** 执行时间(毫秒) */ - executionTime?: number; - /** 操作人 */ - operator?: string; - /** 操作时间 */ - createTime?: string; -} - -/** 访问趋势视图对象 */ -export interface VisitTrendVO { - /** 日期列表 */ - dates: string[]; - /** 浏览量(PV) */ - pvList: number[]; - /** 访客数(UV) */ - uvList: number[]; - /** IP数 */ - ipList: number[]; -} - -/** 访问趋势查询参数 */ -export interface VisitTrendQuery { - /** 开始日期 */ - startDate: string; - /** 结束日期 */ - endDate: string; -} - -/** 访问统计 */ -export interface VisitStatsVO { - /** 今日访客数(UV) */ - todayUvCount: number; - /** 总访客数 */ - totalUvCount: number; - /** 访客数同比增长率(相对于昨天同一时间段的增长率) */ - uvGrowthRate: number; - /** 今日浏览量(PV) */ - todayPvCount: number; - /** 总浏览量 */ - totalPvCount: number; - /** 同比增长率(相对于昨天同一时间段的增长率) */ - pvGrowthRate: number; -} diff --git a/src/api/system/menu.ts b/src/api/system/menu.ts deleted file mode 100644 index 54424c2..0000000 --- a/src/api/system/menu.ts +++ /dev/null @@ -1,76 +0,0 @@ -import request from "@/utils/request"; -// 菜单基础URL -const MENU_BASE_URL = "/api/v1/menus"; - -const MenuAPI = { - /** - * 获取当前用户的路由列表 - *

- * 无需传入角色,后端解析token获取角色自行判断是否拥有路由的权限 - * - * @returns 路由列表 - */ - getRoutes() { - return request({ - url: `${MENU_BASE_URL}/routes`, - method: "GET", - }); - }, - - /** - * 获取菜单下拉数据源 - * - * @returns 菜单下拉数据源 - */ - getOptions(onlyParent?: boolean) { - return request({ - url: `${MENU_BASE_URL}/options`, - method: "GET", - data: { onlyParent: onlyParent }, - }); - }, -}; - -export default MenuAPI; - -/** RouteVO,路由对象 */ -export interface RouteVO { - /** 子路由列表 */ - children: RouteVO[]; - /** 组件路径 */ - component?: string; - /** 路由属性 */ - meta?: Meta; - /** 路由名称 */ - name?: string; - /** 路由路径 */ - path?: string; - /** 跳转链接 */ - redirect?: string; -} - -/** Meta,路由属性 */ -export interface Meta { - /** 【目录】只有一个子路由是否始终显示 */ - alwaysShow?: boolean; - /** 是否隐藏(true-是 false-否) */ - hidden?: boolean; - /** ICON */ - icon?: string; - /** 【菜单】是否开启页面缓存 */ - keepAlive?: boolean; - /** 路由title */ - title?: string; -} - -/** - * 组件数据源 - */ -interface OptionType { - /** 值 */ - value: string | number; - /** 文本 */ - label: string; - /** 子列表 */ - children?: OptionType[]; -} diff --git a/src/api/system/notice.ts b/src/api/system/notice.ts deleted file mode 100644 index b876cc2..0000000 --- a/src/api/system/notice.ts +++ /dev/null @@ -1,201 +0,0 @@ -import request from "@/utils/request"; - -const NOTICE_BASE_URL = "/api/v1/notices"; - -const NoticeAPI = { - /** 获取通知公告分页数据 */ - getPage(queryParams?: NoticePageQuery) { - return request>({ - url: `${NOTICE_BASE_URL}/page`, - method: "GET", - data: queryParams, - }); - }, - - /** - * 获取通知公告表单数据 - * - * @param id NoticeID - * @returns Notice表单数据 - */ - getFormData(id: number) { - return request({ - url: `${NOTICE_BASE_URL}/${id}/form`, - method: "GET", - }); - }, - - /** - * 添加通知公告 - * - * @param data Notice表单数据 - * @returns - */ - add(data: NoticeForm) { - return request({ - url: `${NOTICE_BASE_URL}`, - method: "POST", - data: data, - }); - }, - - /** - * 更新通知公告 - * - * @param id NoticeID - * @param data Notice表单数据 - */ - update(id: number, data: NoticeForm) { - return request({ - url: `${NOTICE_BASE_URL}/${id}`, - method: "PUT", - data: data, - }); - }, - - /** - * 批量删除通知公告,多个以英文逗号(,)分割 - * - * @param ids 通知公告ID字符串,多个以英文逗号(,)分割 - */ - deleteByIds(ids: string) { - return request({ - url: `${NOTICE_BASE_URL}/${ids}`, - method: "DELETE", - }); - }, - - /** - * 发布通知 - * - * @param id 被发布的通知公告id - * @returns - */ - publish(id: number) { - return request({ - url: `${NOTICE_BASE_URL}/${id}/publish`, - method: "PUT", - }); - }, - - /** - * 撤回通知 - * - * @param id 撤回的通知id - * @returns - */ - revoke(id: number) { - return request({ - url: `${NOTICE_BASE_URL}/${id}/revoke`, - method: "PUT", - }); - }, - /** - * 查看通知 - * - * @param id - */ - getDetail(id: string) { - return request({ - url: `${NOTICE_BASE_URL}/${id}/detail`, - method: "GET", - }); - }, - - /* 全部已读 */ - readAll() { - return request({ - url: `${NOTICE_BASE_URL}/read-all`, - method: "PUT", - }); - }, - - /** 获取我的通知分页列表 */ - getMyNoticePage(queryParams?: NoticePageQuery) { - return request>({ - url: `${NOTICE_BASE_URL}/my-page`, - method: "GET", - data: queryParams, - }); - }, -}; - -export default NoticeAPI; - -/** 通知公告分页查询参数 */ -export interface NoticePageQuery extends PageQuery { - /** 标题 */ - title?: string; - /** 发布状态(0:未发布,1:已发布,-1:已撤回) */ - publishStatus?: number; - - isRead?: number; -} - -/** 通知公告表单对象 */ -export interface NoticeForm { - id?: number; - /** 通知标题 */ - title?: string; - /** 通知内容 */ - content?: string; - /** 通知类型 */ - type?: number; - /** 优先级(L:低,M:中,H:高) */ - level?: string; - /** 目标类型(1-全体 2-指定) */ - targetType?: number; - /** 目标ID合集,以,分割 */ - targetUserIds?: string; -} - -/** 通知公告分页对象 */ -export interface NoticePageVO { - id: string; - /** 通知标题 */ - title?: string; - /** 通知内容 */ - content?: string; - /** 通知类型 */ - type?: number; - /** 发布人 */ - publisherName?: string; - /** 优先级(0-低 1-中 2-高) */ - priority?: number; - /** 目标类型(0-全体 1-指定) */ - targetType?: number; - /** 发布状态(0-未发布 1已发布 2已撤回) */ - publishStatus?: number; - /** 发布时间 */ - publishTime?: Date; - /** 撤回时间 */ - revokeTime?: Date; - /** 优先级(L-低 M-中 H-高) */ - level?: string | number; -} - -export interface NoticeDetailVO { - /** 通知ID */ - id?: string; - - /** 通知标题 */ - title?: string; - - /** 通知内容 */ - content?: string; - - /** 通知类型 */ - type?: number; - - /** 发布人 */ - publisherName?: string; - - /** 优先级(L-低 M-中 H-高) */ - level?: string; - - /** 发布时间 */ - publishTime?: Date; - - /** 发布状态 */ - publishStatus?: number; -} diff --git a/src/api/system/role.ts b/src/api/system/role.ts deleted file mode 100644 index 61868b0..0000000 --- a/src/api/system/role.ts +++ /dev/null @@ -1,138 +0,0 @@ -import request from "@/utils/request"; - -const ROLE_BASE_URL = "/api/v1/roles"; - -const RoleAPI = { - /** 获取角色分页数据 */ - getPage(queryParams?: RolePageQuery) { - return request>({ - url: `${ROLE_BASE_URL}/page`, - method: "GET", - data: queryParams, - }); - }, - - /** 获取角色下拉数据源 */ - getOptions() { - return request({ - url: `${ROLE_BASE_URL}/options`, - method: "GET", - }); - }, - /** - * 获取角色的菜单ID集合 - * - * @param roleId 角色ID - * @returns 角色的菜单ID集合 - */ - getRoleMenuIds(roleId: number) { - return request({ - url: `${ROLE_BASE_URL}/${roleId}/menuIds`, - method: "GET", - }); - }, - - /** - * 分配菜单权限 - * - * @param roleId 角色ID - * @param data 菜单ID集合 - */ - updateRoleMenus(roleId: number, data: number[]) { - return request({ - url: `${ROLE_BASE_URL}/${roleId}/menus`, - method: "PUT", - data: data, - }); - }, - - /** - * 获取角色表单数据 - * - * @param id 角色ID - * @returns 角色表单数据 - */ - getFormData(id: number) { - return request({ - url: `${ROLE_BASE_URL}/${id}/form`, - method: "GET", - }); - }, - - /** 添加角色 */ - add(data: RoleForm) { - return request({ - url: `${ROLE_BASE_URL}`, - method: "POST", - data: data, - }); - }, - - /** - * 更新角色 - * - * @param id 角色ID - * @param data 角色表单数据 - */ - update(id: number, data: RoleForm) { - return request({ - url: `${ROLE_BASE_URL}/${id}`, - method: "PUT", - data: data, - }); - }, - - /** - * 批量删除角色,多个以英文逗号(,)分割 - * - * @param ids 角色ID字符串,多个以英文逗号(,)分割 - */ - deleteByIds(ids: string) { - return request({ - url: `${ROLE_BASE_URL}/${ids}`, - method: "DELETE", - }); - }, -}; - -export default RoleAPI; - -/** 角色分页查询参数 */ -export interface RolePageQuery extends PageQuery { - /** 搜索关键字 */ - keywords?: string; -} - -/** 角色分页对象 */ -export interface RolePageVO { - /** 角色编码 */ - code?: string; - /** 角色ID */ - id: number; - /** 角色名称 */ - name?: string; - /** 排序 */ - sort?: number; - /** 角色状态 */ - status?: number; - /** 创建时间 */ - createTime?: string; - /** 修改时间 */ - updateTime?: string; -} - -/** 角色表单对象 */ -export interface RoleForm { - /** 角色ID */ - id?: number; - /** 角色编码 */ - code?: string; - /** 数据权限 */ - dataScope: number; - /** 角色名称 */ - name?: string; - /** 排序 */ - sort: number; - /** 角色状态(1-正常;0-停用) */ - status?: number; -} diff --git a/src/api/system/user.ts b/src/api/user.ts similarity index 89% rename from src/api/system/user.ts rename to src/api/user.ts index c7cf28d..5167973 100644 --- a/src/api/system/user.ts +++ b/src/api/user.ts @@ -135,6 +135,15 @@ const UserAPI = { method: "DELETE", }); }, + + /** 获取微信手机号 */ + getPhoneNumber(data: WechatPhoneData): Promise { + return request({ + url: `${USER_BASE_URL}/wechat-phone`, + method: "POST", + data: data, + }); + }, }; export default UserAPI; @@ -297,7 +306,7 @@ export interface UserForm { avatar?: string; /** 部门ID */ deptId?: number; - /** 邮箱 */ + /** 用户邮箱 */ email?: string; /** 性别 */ gender?: number; @@ -314,3 +323,23 @@ export interface UserForm { /** 用户名 */ username?: string; } + +/** 微信手机号授权数据 */ +export interface WechatPhoneData { + /** 微信授权码 */ + code: string; + /** 加密数据 */ + encryptedData?: string; + /** 初始向量 */ + iv?: string; +} + +/** 手机号获取结果 */ +export interface PhoneNumberResult { + /** 手机号 */ + phoneNumber: string; + /** 纯手机号(去除+86) */ + purePhoneNumber?: string; + /** 国家代码 */ + countryCode?: string; +} diff --git a/src/components/WechatProfile.vue b/src/components/WechatProfile.vue new file mode 100644 index 0000000..a4bbbb3 --- /dev/null +++ b/src/components/WechatProfile.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/src/composables/theme/rootTheme.ts b/src/composables/theme/rootTheme.ts new file mode 100644 index 0000000..0d5bb3d --- /dev/null +++ b/src/composables/theme/rootTheme.ts @@ -0,0 +1,22 @@ +import type { ConfigProviderThemeVars } from "wot-design-uni"; +/* 默认的主题list */ +export const colorColumns = [ + { + value: "#0055FE", + label: "蓝色", + }, + { + value: "#CD5C5C", + label: "红色", + }, + { + value: "#228B22", + label: "绿色", + }, +]; +/* 默认的主题 */ +export const initThemState = "light"; +/* 默认的主题 */ +export const initThemeVars: ConfigProviderThemeVars = { + colorTheme: colorColumns[0].value, +}; diff --git a/src/composables/theme/theme.ts b/src/composables/theme/theme.ts new file mode 100644 index 0000000..983a135 --- /dev/null +++ b/src/composables/theme/theme.ts @@ -0,0 +1,74 @@ +import { colorColumns, initThemState, initThemeVars } from "./rootTheme"; +/* 暗黑模式切换 */ +const theme = ref<"light" | "dark">(initThemState); +/* 组件库的主题色 */ +const themeVars = ref({ ...initThemeVars }); + +// 从本地存储恢复主题设置 +function loadThemeFromStorage() { + try { + const savedTheme = uni.getStorageSync("app_theme_mode"); + const savedThemeColor = uni.getStorageSync("app_theme_color"); + + if (savedTheme && (savedTheme === "light" || savedTheme === "dark")) { + theme.value = savedTheme; + } + + if (savedThemeColor) { + themeVars.value.colorTheme = savedThemeColor; + } + } catch (error) { + console.error("加载主题设置失败:", error); + } +} + +// 保存主题设置到本地存储 +function saveThemeToStorage() { + try { + uni.setStorageSync("app_theme_mode", theme.value); + uni.setStorageSync("app_theme_color", themeVars.value.colorTheme); + } catch (error) { + console.error("保存主题设置失败:", error); + } +} + +export function useTheme() { + /* 切换暗黑模式 */ + function toggleTheme(mode?: "light" | "dark") { + theme.value = mode || (theme.value === "light" ? "dark" : "light"); + setNavigationBarColor(); + saveThemeToStorage(); + } + + /* 设置主题色 */ + function setThemeColor(color: string) { + themeVars.value.colorTheme = color; + saveThemeToStorage(); + } + + /* 初始化theme */ + function initTheme() { + loadThemeFromStorage(); + setNavigationBarColor(); + } + + function setNavigationBarColor() { + uni.setNavigationBarColor({ + frontColor: theme.value === "light" ? "#000000" : "#ffffff", + backgroundColor: theme.value === "light" ? "#ffffff" : "#000000", + animation: { + duration: 400, + timingFunc: "easeIn", + }, + }); + } + + return { + theme, + themeVars, + initTheme, + colorColumns, + toggleTheme, + setThemeColor, + }; +} diff --git a/src/composables/useStomp.ts b/src/composables/useStomp.ts new file mode 100644 index 0000000..c2b8912 --- /dev/null +++ b/src/composables/useStomp.ts @@ -0,0 +1,368 @@ +import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs"; +import { Auth } from "@/utils/auth"; + +export interface UseStompOptions { + /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ + brokerURL?: string; + /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */ + token?: string; + /** 重连延迟,单位毫秒,默认为 8000 */ + reconnectDelay?: number; + /** 连接超时时间,单位毫秒,默认为 10000 */ + connectionTimeout?: number; + /** 是否开启指数退避重连策略 */ + useExponentialBackoff?: boolean; + /** 最大重连次数,默认为 5 */ + maxReconnectAttempts?: number; + /** 最大重连延迟,单位毫秒,默认为 60000 */ + maxReconnectDelay?: number; + /** 是否开启调试日志 */ + debug?: boolean; +} + +/** + * STOMP WebSocket连接组合式函数 + * 用于管理WebSocket连接的建立、断开、重连和消息订阅 + */ +export function useStomp(options: UseStompOptions = {}) { + // 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取 + const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; + + const brokerURL = ref(options.brokerURL ?? defaultBrokerURL); + // 默认配置参数 + const reconnectDelay = options.reconnectDelay ?? 15000; // 默认15秒重连间隔 + const connectionTimeout = options.connectionTimeout ?? 10000; + const useExponentialBackoff = options.useExponentialBackoff ?? false; + const maxReconnectAttempts = options.maxReconnectAttempts ?? 3; // 最多重连3次 + const maxReconnectDelay = options.maxReconnectDelay ?? 60000; + + // 连接状态标记 + const isConnected = ref(false); + // 重连尝试次数 + const reconnectCount = ref(0); + // 重连计时器 + let reconnectTimer: any = null; + // 连接超时计时器 + let connectionTimeoutTimer: any = null; + // 存储所有订阅 + const subscriptions = new Map(); + + // 用于保存 STOMP 客户端的实例 + const client = ref(null); + // 防止重复连接的标志 + let isConnecting = false; + let isManualDisconnect = false; + + /** + * 初始化 STOMP 客户端 + */ + const initializeClient = () => { + // 如果客户端已存在且正在连接或已连接,直接返回 + if (client.value && (client.value.active || client.value.connected)) { + console.log("STOMP客户端已存在且处于活动状态,跳过初始化"); + return; + } + + // 检查WebSocket端点是否配置 + if (!brokerURL.value) { + console.error("WebSocket连接失败: 未配置WebSocket端点URL"); + return; + } + + // 每次连接前重新获取最新令牌,不依赖之前的token值 + const currentToken = Auth.getAccessToken(); + + // 检查令牌是否为空,如果为空则不进行连接 + if (!currentToken) { + console.error("WebSocket连接失败:授权令牌为空,请先登录"); + return; + } + + // 如果有旧的客户端,先清理 + if (client.value) { + try { + client.value.deactivate(); + } catch (error) { + console.warn("清理旧客户端时出错:", error); + } + client.value = null; + } + + // 创建 STOMP 客户端 + client.value = new Client({ + brokerURL: brokerURL.value, + connectHeaders: { + Authorization: `Bearer ${currentToken}`, + }, + debug: options.debug ? console.log : () => {}, + reconnectDelay: 0, // 禁用内置重连机制,使用自定义重连 + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + }); + + // 设置连接监听器 + client.value.onConnect = () => { + isConnected.value = true; + isConnecting = false; + reconnectCount.value = 0; + clearTimeout(connectionTimeoutTimer); + clearTimeout(reconnectTimer); + console.log("WebSocket连接已建立"); + }; + + // 设置断开连接监听器 + client.value.onDisconnect = () => { + isConnected.value = false; + isConnecting = false; + console.log("WebSocket连接已断开"); + + // 如果不是手动断开且未达到最大重连次数,则尝试重连 + if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) { + handleReconnect(); + } + }; + + // 设置 Web Socket 关闭监听器 + client.value.onWebSocketClose = (event) => { + isConnected.value = false; + isConnecting = false; + console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`); + + // 如果是手动断开,不要重连 + if (isManualDisconnect) { + console.log("手动断开连接,不进行重连"); + return; + } + + // 如果是授权问题导致的关闭,尝试重连 + if ( + (event?.code === 1000 || event?.code === 1006 || event?.code === 1008) && + reconnectCount.value < maxReconnectAttempts + ) { + console.log("检测到连接异常关闭,将尝试重连"); + + // 通过 handleReconnect 统一处理重连,避免重复计数 + handleReconnect(); + } + }; + + // 设置错误监听器 + client.value.onStompError = (frame) => { + console.error("STOMP错误:", frame.headers, frame.body); + isConnecting = false; + + // 检查是否是授权错误 + if ( + frame.headers?.message?.includes("Unauthorized") || + frame.body?.includes("Unauthorized") || + frame.body?.includes("Token") + ) { + console.warn("WebSocket授权错误,请检查登录状态"); + // 授权错误不进行重连 + isManualDisconnect = true; + } + }; + }; + + /** + * 处理重连逻辑 + */ + const handleReconnect = () => { + // 如果已经在连接中或手动断开,不重连 + if (isConnecting || isManualDisconnect) { + return; + } + + if (reconnectCount.value >= maxReconnectAttempts) { + console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`); + return; + } + + reconnectCount.value++; + console.log(`准备重连(${reconnectCount.value}/${maxReconnectAttempts})...`); + + // 使用指数退避策略增加重连间隔 + const delay = useExponentialBackoff + ? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay) + : reconnectDelay; + + // 清除之前的计时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + + // 设置重连计时器 + reconnectTimer = setTimeout(() => { + if (!isConnected.value && !isManualDisconnect && !isConnecting) { + console.log(`开始重连...`); + connect(); + } + }, delay); + }; + + // 监听 brokerURL 的变化,若地址改变则重新初始化 + watch(brokerURL, (newURL, oldURL) => { + if (newURL !== oldURL) { + console.log(`brokerURL changed from ${oldURL} to ${newURL}`); + // 断开当前连接,重新激活客户端 + if (client.value && client.value.connected) { + client.value.deactivate(); + } + brokerURL.value = newURL; + initializeClient(); // 重新初始化客户端 + } + }); + + // 初始化客户端 + initializeClient(); + + /** + * 激活连接(如果已经连接或正在激活则直接返回) + */ + const connect = () => { + // 重置手动断开标志 + isManualDisconnect = false; + + // 检查是否有配置WebSocket端点 + if (!brokerURL.value) { + console.error("WebSocket连接失败: 未配置WebSocket端点URL"); + return; + } + + // 防止重复连接 + if (isConnecting) { + console.log("WebSocket正在连接中,跳过重复连接请求"); + return; + } + + if (!client.value) { + initializeClient(); + } + + if (!client.value) { + console.error("STOMP客户端初始化失败"); + return; + } + + // 避免重复连接:检查是否已连接 + if (client.value.connected) { + console.log("WebSocket已经连接,跳过重复连接"); + isConnected.value = true; + return; + } + + // 设置连接标志 + isConnecting = true; + + // 设置连接超时 + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = setTimeout(() => { + if (!isConnected.value && isConnecting) { + console.warn("WebSocket连接超时"); + isConnecting = false; + if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) { + handleReconnect(); + } + } + }, connectionTimeout); + + try { + client.value.activate(); + console.log("正在建立WebSocket连接..."); + } catch (error) { + console.error("激活WebSocket连接失败:", error); + isConnecting = false; + } + }; + + /** + * 订阅指定主题 + * @param destination 目标主题地址 + * @param callback 接收到消息时的回调函数 + * @returns 返回订阅 id,用于后续取消订阅 + */ + const subscribe = (destination: string, callback: (_message: IMessage) => void): string => { + if (!client.value || !client.value.connected) { + console.warn(`尝试订阅 ${destination} 失败: 客户端未连接`); + return ""; + } + + try { + const subscription = client.value.subscribe(destination, callback); + const subscriptionId = subscription.id; + subscriptions.set(subscriptionId, subscription); + console.log(`订阅成功: ${destination}, ID: ${subscriptionId}`); + return subscriptionId; + } catch (error) { + console.error(`订阅 ${destination} 失败:`, error); + return ""; + } + }; + + /** + * 取消订阅 + * @param subscriptionId 订阅 id + */ + const unsubscribe = (subscriptionId: string) => { + const subscription = subscriptions.get(subscriptionId); + if (subscription) { + subscription.unsubscribe(); + subscriptions.delete(subscriptionId); + console.log(`已取消订阅: ${subscriptionId}`); + } + }; + + /** + * 断开WebSocket连接 + */ + const disconnect = () => { + // 设置手动断开标志 + isManualDisconnect = true; + + // 清除所有计时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + if (connectionTimeoutTimer) { + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = null; + } + + // 清除所有订阅 + for (const [id, subscription] of subscriptions.entries()) { + try { + subscription.unsubscribe(); + } catch (error) { + console.warn(`取消订阅 ${id} 时出错:`, error); + } + } + subscriptions.clear(); + + // 断开连接 + if (client.value) { + try { + if (client.value.connected || client.value.active) { + client.value.deactivate(); + console.log("WebSocket连接已主动断开"); + } + } catch (error) { + console.error("断开WebSocket连接时出错:", error); + } + client.value = null; + } + + isConnected.value = false; + isConnecting = false; + reconnectCount.value = 0; + }; + + return { + isConnected, + connect, + subscribe, + unsubscribe, + disconnect, + }; +} diff --git a/src/composables/useTabbar.ts b/src/composables/useTabbar.ts new file mode 100644 index 0000000..27179b9 --- /dev/null +++ b/src/composables/useTabbar.ts @@ -0,0 +1,61 @@ +/* + * @Author: weisheng + * @Date: 2024-10-29 22:12:54 + * @LastEditTime: 2025-01-13 16:45:28 + * @LastEditors: 810505339 + * @Description: + * @FilePath: \wot-demo\src\composables\useTabbar.ts + * 记得注释 + */ +export interface TabbarItem { + name: string; + value: number | null; + active: boolean; + title: string; + icon: string; +} + +const tabbarItems = ref([ + { name: "home", value: null, active: true, title: "home", icon: "home" }, + { name: "hi", value: null, active: false, title: "hi", icon: "app" }, + { name: "setting", value: null, active: false, title: "setting", icon: "setting" }, +]); + +export function useTabbar() { + const tabbarList = computed(() => tabbarItems.value); + + const activeTabbar = computed(() => { + const item = tabbarItems.value.find((item) => item.active); + return item || tabbarItems.value[0]; + }); + + const getTabbarItemValue = (name: string) => { + const item = tabbarItems.value.find((item) => item.name === name); + return item && item.value ? item.value : null; + }; + + const setTabbarItem = (name: string, value: number) => { + const tabbarItem = tabbarItems.value.find((item) => item.name === name); + if (tabbarItem) { + tabbarItem.value = value; + } + }; + + const setTabbarItemActive = (name: string) => { + tabbarItems.value.forEach((item) => { + if (item.name === name) { + item.active = true; + } else { + item.active = false; + } + }); + }; + + return { + tabbarList, + activeTabbar, + getTabbarItemValue, + setTabbarItem, + setTabbarItemActive, + }; +} diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..f8be9d9 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,4 @@ +export const ROLE_ROOT = "ROOT"; + +// 🔗 导出所有存储键常量 +export * from "./storage-keys"; diff --git a/src/constants/storage-keys.ts b/src/constants/storage-keys.ts new file mode 100644 index 0000000..6567efa --- /dev/null +++ b/src/constants/storage-keys.ts @@ -0,0 +1,11 @@ +/** + * 存储键常量统一管理 + * 包括 localStorage、sessionStorage 等各种存储的键名 + */ + +// 🔐 用户认证相关 +export const ACCESS_TOKEN_KEY = "access_token"; +export const REFRESH_TOKEN_KEY = "refresh_token"; + +// 📊 用户缓存相关 +export const USER_INFO_KEY = "user_info"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index abab537..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 全局Hooks入口文件 - * 导出所有可用的Hooks - */ - -// 导出WebSocket相关Hook -export * from "./websocket"; diff --git a/src/hooks/websocket/core/useStomp.ts b/src/hooks/websocket/core/useStomp.ts deleted file mode 100644 index 76bcea2..0000000 --- a/src/hooks/websocket/core/useStomp.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { getToken as getAccessToken } from "@/utils/cache"; - -export interface UseStompOptions { - /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ - brokerURL?: string; - /** 重连延迟,单位毫秒,默认为 8000 */ - reconnectDelay?: number; - /** 连接超时时间,单位毫秒,默认为 10000 */ - connectionTimeout?: number; - /** 是否开启指数退避重连策略 */ - useExponentialBackoff?: boolean; - /** 最大重连次数,默认为 5 */ - maxReconnectAttempts?: number; - /** 最大重连延迟,单位毫秒,默认为 60000 */ - maxReconnectDelay?: number; - /** 是否开启调试日志 */ - debug?: boolean; -} - -/** - * STOMP WebSocket连接Hook (UniApp版本) - * 用于管理WebSocket连接的建立、断开、重连和消息订阅 - */ -export function useStomp(options: UseStompOptions = {}) { - // 默认配置 - const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; - const brokerURL = ref(options.brokerURL ?? defaultBrokerURL); - const reconnectDelay = options.reconnectDelay ?? 8000; - const connectionTimeout = options.connectionTimeout ?? 10000; - const useExponentialBackoff = options.useExponentialBackoff ?? false; - const maxReconnectAttempts = options.maxReconnectAttempts ?? 5; - const maxReconnectDelay = options.maxReconnectDelay ?? 60000; - - // 连接状态和计数 - const isConnected = ref(false); - const reconnectCount = ref(0); - let reconnectTimer: number | null = null; - let connectionTimeoutTimer: number | null = null; - - // 存储所有订阅 - const subscriptions = ref>({}); - - // WebSocket实例 - let socketTask: any = null; - const client = ref(null); - - /** - * 创建WebSocket连接 - */ - const createSocketConnection = () => { - const token = getAccessToken(); - if (!token) { - console.error("WebSocket连接失败:未找到有效token"); - return null; - } - - // 创建WebSocket连接 - try { - // 构建带有token的URL - let url = brokerURL.value; - if (url.indexOf("?") > -1) { - url += "&token=" + token; - } else { - url += "?token=" + token; - } - - // 创建WebSocket连接 - socketTask = uni.connectSocket({ - url: url, - complete: () => {}, - }); - - if (!socketTask) { - console.error("WebSocket连接创建失败"); - return null; - } - - // 设置WebSocket事件处理函数 - socketTask.onOpen(() => { - isConnected.value = true; - reconnectCount.value = 0; - if (connectionTimeoutTimer) clearTimeout(connectionTimeoutTimer); - console.log("WebSocket连接已建立"); - }); - - socketTask.onClose(() => { - isConnected.value = false; - console.log("WebSocket连接已关闭"); - - // 如果使用指数退避重连策略,则处理重连 - if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) { - handleReconnect(); - } - }); - - socketTask.onError((error: any) => { - console.error("WebSocket连接错误:", error); - }); - - socketTask.onMessage((res: any) => { - const message = JSON.parse(res.data); - // 处理订阅消息 - if (message.subscription && subscriptions.value[message.subscription]) { - const subscription = subscriptions.value[message.subscription]; - if (subscription.callback) { - subscription.callback(message); - } - } - }); - - return socketTask; - } catch (error) { - console.error("创建WebSocket连接时出错:", error); - return null; - } - }; - - /** - * 处理重连逻辑 - */ - const handleReconnect = () => { - if (reconnectCount.value >= maxReconnectAttempts) { - console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`); - return; - } - - reconnectCount.value++; - console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`); - - // 使用指数退避策略 - const delay = useExponentialBackoff - ? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay) - : reconnectDelay; - - // 清除之前的计时器 - if (reconnectTimer) { - clearTimeout(reconnectTimer); - } - - // 设置重连计时器 - reconnectTimer = setTimeout(() => { - if (!isConnected.value) { - connect(); - } - }, delay) as unknown as number; - }; - - /** - * 建立WebSocket连接 - */ - const connect = () => { - if (isConnected.value) { - return; - } - - // 创建WebSocket连接 - socketTask = createSocketConnection(); - client.value = socketTask; - - // 设置连接超时 - if (connectionTimeoutTimer) { - clearTimeout(connectionTimeoutTimer); - } - - connectionTimeoutTimer = setTimeout(() => { - if (!isConnected.value) { - console.warn("WebSocket连接超时"); - if (useExponentialBackoff) { - handleReconnect(); - } - } - }, connectionTimeout) as unknown as number; - }; - - /** - * 订阅主题 - * @param destination 主题地址 - * @param callback 回调函数 - * @returns 订阅ID - */ - const subscribe = (destination: string, callback: (message: any) => void): string => { - if (!socketTask || !isConnected.value) { - console.warn("WebSocket未连接,无法订阅:", destination); - return ""; - } - - // 生成唯一订阅ID - const subscriptionId = "sub-" + Math.random().toString(36).substr(2, 9); - - // 发送订阅消息 - socketTask.send({ - data: JSON.stringify({ - command: "SUBSCRIBE", - headers: { - id: subscriptionId, - destination: destination, - }, - }), - success: () => { - console.log(`订阅成功: ${destination}, ID: ${subscriptionId}`); - }, - fail: (err: any) => { - console.error(`订阅失败(${destination}):`, err); - }, - }); - - // 保存订阅 - subscriptions.value[subscriptionId] = { - destination, - callback, - }; - - return subscriptionId; - }; - - /** - * 取消订阅 - * @param subscriptionId 订阅ID - */ - const unsubscribe = (subscriptionId: string) => { - if (!socketTask || !isConnected.value || !subscriptions.value[subscriptionId]) { - return; - } - - try { - // 发送取消订阅消息 - socketTask.send({ - data: JSON.stringify({ - command: "UNSUBSCRIBE", - headers: { - id: subscriptionId, - }, - }), - success: () => { - console.log(`已取消订阅: ${subscriptionId}`); - }, - fail: (err: any) => { - console.error(`取消订阅失败(${subscriptionId}):`, err); - }, - }); - } catch (error) { - console.error(`取消订阅失败(${subscriptionId}):`, error); - } finally { - Reflect.deleteProperty(subscriptions.value, subscriptionId); - } - }; - - /** - * 断开WebSocket连接 - */ - const disconnect = () => { - if (!socketTask) { - return; - } - - // 取消所有订阅 - Object.keys(subscriptions.value).forEach(unsubscribe); - - // 断开WebSocket连接 - try { - socketTask.close({ - success: () => { - console.log("WebSocket连接已断开"); - isConnected.value = false; - socketTask = null; - }, - fail: (err: any) => { - console.error("断开WebSocket连接失败:", err); - }, - }); - } catch (error) { - console.error("断开WebSocket连接失败:", error); - } - }; - - // 返回公开的API - return { - isConnected, - client, - connect, - disconnect, - subscribe, - unsubscribe, - }; -} diff --git a/src/hooks/websocket/index.ts b/src/hooks/websocket/index.ts deleted file mode 100644 index acc9aa0..0000000 --- a/src/hooks/websocket/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * WebSocket相关Hook入口文件 - * 统一导出所有WebSocket相关Hook - */ - -// 核心基础Hook -export { useStomp } from "./core/useStomp"; - -// 业务服务Hook -export { useDictSync } from "./services/useDictSync"; diff --git a/src/hooks/websocket/services/useDictSync.ts b/src/hooks/websocket/services/useDictSync.ts deleted file mode 100644 index e2e343b..0000000 --- a/src/hooks/websocket/services/useDictSync.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { useDictStore } from "@/store/modules/dict"; -import { useStomp } from "../core/useStomp"; -import { ref } from "vue"; - -// 字典消息类型 -export interface DictMessage { - dictCode: string; - timestamp: number; -} - -// 字典事件回调类型 -export type DictMessageCallback = (message: DictMessage) => void; - -// 全局单例实例 -let instance: ReturnType | null = null; - -/** - * 创建字典同步Hook - * 负责监听后端字典变更并同步到前端 - */ -function createDictSyncHook() { - const dictStore = useDictStore(); - - // 使用现有的useStomp,配置适合字典场景的重连参数 - const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp({ - reconnectDelay: 10000, // 使用更长的重连延迟 - 10秒 - connectionTimeout: 15000, // 更长的连接超时时间 - 15秒 - useExponentialBackoff: false, // 字典数据不需要指数退避策略 - }); - - // 存储订阅ID - const subscriptionIds = ref([]); - - // 已订阅的主题 - const subscribedTopics = ref>(new Set()); - - // 消息回调函数列表 - const messageCallbacks = ref([]); - - /** - * 注册字典消息回调 - * @param callback 回调函数 - */ - const onDictMessage = (callback: DictMessageCallback) => { - messageCallbacks.value.push(callback); - return () => { - // 返回取消注册的函数 - const index = messageCallbacks.value.indexOf(callback); - if (index !== -1) { - messageCallbacks.value.splice(index, 1); - } - }; - }; - - /** - * 初始化WebSocket - */ - const initWebSocket = async () => { - try { - // 连接WebSocket - connect(); - - // 设置字典订阅 - setupDictSubscription(); - } catch (error) { - console.error("[WebSocket] 初始化失败:", error); - } - }; - - /** - * 关闭WebSocket - */ - const closeWebSocket = () => { - // 取消所有订阅 - subscriptionIds.value.forEach((id) => { - unsubscribe(id); - }); - subscriptionIds.value = []; - subscribedTopics.value.clear(); - - // 断开连接 - disconnect(); - }; - - /** - * 设置字典订阅 - */ - const setupDictSubscription = () => { - const topic = "/topic/dict"; - - // 防止重复订阅 - if (subscribedTopics.value.has(topic)) { - console.log(`跳过重复订阅: ${topic}`); - return; - } - - console.log(`开始尝试订阅字典主题: ${topic}`); - - // 使用简化的重试逻辑,依赖useStomp的连接管理 - const attemptSubscribe = () => { - if (!isConnected.value) { - console.log("等待WebSocket连接建立..."); - // 3秒后再次尝试 - setTimeout(attemptSubscribe, 3000); - return; - } - - // 检查是否已订阅 - if (subscribedTopics.value.has(topic)) { - return; - } - - console.log(`连接已建立,开始订阅: ${topic}`); - - // 订阅字典更新 - const subId = subscribe(topic, (message: any) => { - handleDictEvent(message); - }); - - if (subId) { - subscriptionIds.value.push(subId); - subscribedTopics.value.add(topic); - console.log(`字典主题订阅成功: ${topic}`); - } else { - console.warn(`字典主题订阅失败: ${topic}`); - } - }; - - // 开始尝试订阅 - attemptSubscribe(); - }; - - /** - * 处理字典事件 - * @param message STOMP消息 - */ - const handleDictEvent = (message: any) => { - if (!message.body) return; - - try { - // 记录接收到的消息 - console.log(`收到字典更新消息: ${message.body}`); - - // 尝试解析消息 - const parsedData = JSON.parse(message.body) as DictMessage; - const dictCode = parsedData.dictCode; - - if (!dictCode) return; - - // 清除缓存,等待按需加载 - dictStore.removeDictItem(dictCode); - console.log(`字典缓存已清除: ${dictCode}`); - - // 调用所有注册的回调函数 - messageCallbacks.value.forEach((callback) => { - try { - callback(parsedData); - } catch (callbackError) { - console.error("[WebSocket] 回调执行失败:", callbackError); - } - }); - - // 显示提示消息 - console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`); - } catch (error) { - console.error("[WebSocket] 解析消息失败:", error); - } - }; - - return { - isConnected, - initWebSocket, - closeWebSocket, - handleDictEvent, - onDictMessage, - }; -} - -/** - * 字典同步Hook - * 用于监听后端字典变更并同步到前端 - */ -export function useDictSync() { - if (!instance) { - instance = createDictSyncHook(); - } - return instance; -} diff --git a/src/layouts/default.vue b/src/layouts/default.vue new file mode 100644 index 0000000..2d5d262 --- /dev/null +++ b/src/layouts/default.vue @@ -0,0 +1,4 @@ + diff --git a/src/layouts/tabbar.vue b/src/layouts/tabbar.vue new file mode 100644 index 0000000..b022b8a --- /dev/null +++ b/src/layouts/tabbar.vue @@ -0,0 +1,84 @@ + + + + + + diff --git a/src/pages.json b/src/pages.json index 1abf054..eb3f26d 100644 --- a/src/pages.json +++ b/src/pages.json @@ -1,170 +1,65 @@ { - "easycom": { - "autoscan": true, - "custom": { - "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue", - "^cu-(.*)": "@/components/cu-$1/index.vue" - } - }, - "pages": [ { "path": "pages/index/index", - "style": { - "navigationStyle": "custom" - } + "type": "home" }, { - "path": "pages/work/index", - "style": { - "navigationBarTitleText": "工作台" - } - }, - { - "path": "pages/mine/index", - "style": { - "navigationBarTitleText": "我的" - } - }, - { - "path": "pages/mine/about/index", - "style": { - "navigationBarTitleText": "关于我们" - } - }, - { - "path": "pages/mine/faq/index", - "style": { - "navigationBarTitleText": "常见问题" - } - }, - { - "path": "pages/mine/feedback/index", - "style": { - "navigationBarTitleText": "问题反馈" - } - }, - { - "path": "pages/mine/settings/index", - "style": { - "navigationBarTitleText": "设置" - } - }, - { - "path": "pages/mine/settings/account/index", - "style": { - "navigationBarTitleText": "账号和安全" - } - }, - { - "path": "pages/mine/settings/agreement/index", - "style": { - "navigationBarTitleText": "用户协议" - } - }, - { - "path": "pages/mine/settings/privacy/index", - "style": { - "navigationBarTitleText": "隐私政策" - } - }, - { - "path": "pages/mine/settings/theme/index", - "style": { - "navigationBarTitleText": "主题设置" - } - }, - { - "path": "pages/mine/settings/network/index", - "style": { - "navigationBarTitleText": "网络检测" - } + "path": "pages/login/complete-profile", + "type": "page" }, { "path": "pages/login/index", - "style": { - "navigationBarTitleText": "登录" - } + "type": "page" + }, + { + "path": "pages/mine/index", + "type": "page" + }, + { + "path": "pages/mine/about/index", + "type": "page" + }, + { + "path": "pages/mine/faq/index", + "type": "page" + }, + { + "path": "pages/mine/feedback/index", + "type": "page" }, { "path": "pages/mine/profile/index", - "style": { - "navigationBarTitleText": "个人资料" - } - }, - - { - "path": "pages/work/user/index", - "style": { - "navigationStyle": "custom" - } + "type": "page" }, { - "path": "pages/work/log/index", - "style": { - "navigationBarTitleText": "日志管理" - } + "path": "pages/mine/settings/index", + "type": "page" }, { - "path": "pages/work/config/index", - "style": { - "navigationBarTitleText": "系统配置" - } + "path": "pages/mine/settings/account/index", + "type": "page" }, { - "path": "pages/work/notice/index", - "style": { - "navigationBarTitleText": "通知公告" - } + "path": "pages/mine/settings/agreement/index", + "type": "page" }, { - "path": "pages/work/notice/detail", - "style": { - "navigationBarTitleText": "通知详情" - } + "path": "pages/mine/settings/network/index", + "type": "page" }, { - "path": "pages/work/role/index", - "style": { - "navigationBarTitleText": "角色管理" - } + "path": "pages/mine/settings/privacy/index", + "type": "page" }, { - "path": "pages/work/role/assign-perm", - "style": { - "navigationBarTitleText": "角色分配权限" - } + "path": "pages/mine/settings/theme/index", + "type": "page" } ], "globalStyle": { "navigationBarTextStyle": "black", - "navigationBarTitleText": "vue-uniapp-template", - "navigationBarBackgroundColor": "#F8F8F8", - "backgroundColor": "#F8F8F8" + "navigationBarTitleText": "@uni-helper" }, - "tabBar": { - "color": "#474747", - "selectedColor": "#3B8DFF", - "backgroundColor": "#F8F8F8", - "list": [ - { - "pagePath": "pages/index/index", - "text": "首页", - "iconPath": "static/tabbar/home.png", - "selectedIconPath": "static/tabbar/home-active.png" - }, - { - "pagePath": "pages/work/index", - "text": "工作台", - "iconPath": "static/tabbar/work.png", - "selectedIconPath": "static/tabbar/work-active.png" - }, - { - "pagePath": "pages/mine/index", - "text": "我的", - "iconPath": "static/tabbar/mine.png", - "selectedIconPath": "static/tabbar/mine-active.png" - } - ] - } -} + "subPackages": [] +} \ No newline at end of file diff --git a/src/pages/login/complete-profile.vue b/src/pages/login/complete-profile.vue new file mode 100644 index 0000000..bcce91a --- /dev/null +++ b/src/pages/login/complete-profile.vue @@ -0,0 +1,512 @@ + + + + + diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index 280d378..348af18 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -166,29 +166,66 @@ const handleWechatLogin = async () => { provider: "weixin", }); - // 调用后端接口进行登录认证 - const result = await userStore.loginByWechat(code); + // 尝试使用增强的微信登录接口 + try { + const result = await userStore.loginByWechatMini({ + code: code, + }); - if (result) { - // 获取用户信息 - await userStore.getInfo(); - toast.success("登录成功"); + if (result) { + // 获取用户信息 + await userStore.getInfo(); + toast.success("登录成功"); - // 检查用户信息是否完整 - if (!userStore.isUserInfoComplete()) { - // 如果信息不完整,跳转到完善信息页面 - setTimeout(() => { - uni.navigateTo({ - url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`, - }); - }, 1000); - } else { - // 否则直接跳转到重定向页面 - setTimeout(() => { - uni.reLaunch({ - url: redirect.value, - }); - }, 1000); + // 检查是否为新用户或信息不完整 + const wechatResult = result as any; // 类型断言 + if ( + wechatResult.isNewUser || + !wechatResult.isProfileComplete || + !userStore.isUserInfoComplete() + ) { + // 如果信息不完整,跳转到完善信息页面 + setTimeout(() => { + uni.navigateTo({ + url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`, + }); + }, 1000); + } else { + // 否则直接跳转到重定向页面 + setTimeout(() => { + uni.reLaunch({ + url: redirect.value, + }); + }, 1000); + } + } + } catch (enhancedError) { + // 如果增强接口失败,回退到原始接口 + console.log("增强微信登录失败,回退到原始接口:", enhancedError); + + const result = await userStore.loginByWechat(code); + + if (result) { + // 获取用户信息 + await userStore.getInfo(); + toast.success("登录成功"); + + // 检查用户信息是否完整 + if (!userStore.isUserInfoComplete()) { + // 如果信息不完整,跳转到完善信息页面 + setTimeout(() => { + uni.navigateTo({ + url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`, + }); + }, 1000); + } else { + // 否则直接跳转到重定向页面 + setTimeout(() => { + uni.reLaunch({ + url: redirect.value, + }); + }, 1000); + } } } // #endif diff --git a/src/pages/mine/profile/index.vue b/src/pages/mine/profile/index.vue index a2be5a7..d70b5fe 100644 --- a/src/pages/mine/profile/index.vue +++ b/src/pages/mine/profile/index.vue @@ -63,8 +63,8 @@ - - diff --git a/src/pages/work/config/index.vue b/src/pages/work/config/index.vue deleted file mode 100644 index aa8dd4b..0000000 --- a/src/pages/work/config/index.vue +++ /dev/null @@ -1,324 +0,0 @@ - - - - diff --git a/src/pages/work/index.vue b/src/pages/work/index.vue deleted file mode 100644 index 8197afa..0000000 --- a/src/pages/work/index.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/src/pages/work/log/index.vue b/src/pages/work/log/index.vue deleted file mode 100644 index 8f7fe92..0000000 --- a/src/pages/work/log/index.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - - diff --git a/src/pages/work/notice/detail.vue b/src/pages/work/notice/detail.vue deleted file mode 100644 index 60ed8b1..0000000 --- a/src/pages/work/notice/detail.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - diff --git a/src/pages/work/notice/index.vue b/src/pages/work/notice/index.vue deleted file mode 100644 index 19ccc61..0000000 --- a/src/pages/work/notice/index.vue +++ /dev/null @@ -1,284 +0,0 @@ - - - - diff --git a/src/pages/work/role/assign-perm.vue b/src/pages/work/role/assign-perm.vue deleted file mode 100644 index 53fb546..0000000 --- a/src/pages/work/role/assign-perm.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - diff --git a/src/pages/work/role/index.vue b/src/pages/work/role/index.vue deleted file mode 100644 index dc4b211..0000000 --- a/src/pages/work/role/index.vue +++ /dev/null @@ -1,356 +0,0 @@ - - - - - - diff --git a/src/pages/work/user/index.vue b/src/pages/work/user/index.vue deleted file mode 100644 index b15526f..0000000 --- a/src/pages/work/user/index.vue +++ /dev/null @@ -1,426 +0,0 @@ - - - - - - - diff --git a/src/store/index.ts b/src/store/index.ts index 8f9d085..63acd80 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,6 +9,5 @@ export function setupStore(app: App) { } export * from "./modules/user"; -export * from "./modules/dict"; export * from "./modules/theme"; export { store }; diff --git a/src/store/modules/dict.ts b/src/store/modules/dict.ts deleted file mode 100644 index c79d91b..0000000 --- a/src/store/modules/dict.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { defineStore } from "pinia"; -import DictAPI, { type DictItemOption } from "@/api/system/dict"; - -const DICT_CACHE_KEY = "dict_cache"; - -export const useDictStore = defineStore("dict", () => { - // 字典数据缓存 - const dictCache = ref>(uni.getStorageSync(DICT_CACHE_KEY) || {}); - - // 监听dictCache变化,同步到本地存储 - watch( - dictCache, - (newVal) => { - uni.setStorageSync(DICT_CACHE_KEY, newVal); - }, - { deep: true } - ); - - // 请求队列(防止重复请求) - const requestQueue: Record> = {}; - - /** - * 缓存字典数据 - * @param dictCode 字典编码 - * @param data 字典项列表 - */ - const cacheDictItems = (dictCode: string, data: DictItemOption[]) => { - dictCache.value[dictCode] = data; - }; - - /** - * 加载字典数据(如果缓存中没有则请求) - * @param dictCode 字典编码 - */ - const loadDictItems = async (dictCode: string) => { - if (dictCache.value[dictCode]) return; - - // 防止重复请求 - if (!requestQueue[dictCode]) { - requestQueue[dictCode] = DictAPI.getDictItems(dictCode).then((data) => { - cacheDictItems(dictCode, data); - Reflect.deleteProperty(requestQueue, dictCode); - }); - } - await requestQueue[dictCode]; - }; - - /** - * 获取字典项列表 - * @param dictCode 字典编码 - * @returns 字典项列表 - */ - const getDictItems = (dictCode: string): DictItemOption[] => { - return dictCache.value[dictCode] || []; - }; - - /** - * 移除指定字典项 - * @param dictCode 字典编码 - */ - const removeDictItem = (dictCode: string) => { - if (dictCache.value[dictCode]) { - Reflect.deleteProperty(dictCache.value, dictCode); - } - }; - - /** - * 清空字典缓存 - */ - const clearDictCache = () => { - dictCache.value = {}; - }; - - return { - loadDictItems, - getDictItems, - removeDictItem, - clearDictCache, - }; -}); diff --git a/src/store/modules/theme.ts b/src/store/modules/theme.ts index 51c1d34..7cda548 100644 --- a/src/store/modules/theme.ts +++ b/src/store/modules/theme.ts @@ -1,7 +1,6 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { applyThemeToMiniProgram } from "@/utils/theme"; -import { getLighterColor, getDarkerColor } from "@/utils/colorUtils"; // 从缓存获取主题色 const getThemeColor = (): string => { @@ -28,11 +27,9 @@ export const useThemeStore = defineStore("theme", () => { // H5环境 document.documentElement.style.setProperty("--primary-color", color); - // 计算衍生色 - const lighterColor = getLighterColor(color, 0.8); - const darkerColor = getDarkerColor(color, 0.8); - document.documentElement.style.setProperty("--primary-color-light", lighterColor); - document.documentElement.style.setProperty("--primary-color-dark", darkerColor); + // 设置简单的衍生色(不依赖外部工具函数) + document.documentElement.style.setProperty("--primary-color-light", color + "80"); // 添加透明度 + document.documentElement.style.setProperty("--primary-color-dark", color); } else { // 小程序环境 applyThemeToMiniProgram(color); diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 0f364f9..03d4f5c 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -1,7 +1,10 @@ import { defineStore } from "pinia"; import AuthAPI, { type LoginFormData } from "@/api/auth"; -import UserAPI, { type UserInfo } from "@/api/system/user"; -import { setToken, getUserInfo, setUserInfo, clearAll } from "@/utils/cache"; +import UserAPI, { type UserInfo } from "@/api/user"; +import { setAccessToken, clearTokens } from "@/utils/auth"; +import { getUserInfo, setUserInfo } from "@/utils/storage"; +import { USER_INFO_KEY } from "@/constants"; +import { Storage } from "@/utils/storage"; export const useUserStore = defineStore("user", () => { const userInfo = ref(getUserInfo()); @@ -11,7 +14,7 @@ export const useUserStore = defineStore("user", () => { return new Promise((resolve, reject) => { AuthAPI.login(data) .then((data) => { - setToken(data.accessToken); + setAccessToken(data.accessToken); resolve(data); }) .catch((error) => { @@ -26,7 +29,7 @@ export const useUserStore = defineStore("user", () => { return new Promise((resolve, reject) => { AuthAPI.wechatLogin(code) .then((data) => { - setToken(data.accessToken); + setAccessToken(data.accessToken); resolve(data); }) .catch((error) => { @@ -36,6 +39,21 @@ export const useUserStore = defineStore("user", () => { }); }; + // 微信小程序增强登录 + const loginByWechatMini = (data: any): Promise => { + return new Promise((resolve, reject) => { + AuthAPI.wechatMiniLogin(data) + .then((result) => { + setAccessToken(result.accessToken); + resolve(result); + }) + .catch((error) => { + console.error("微信小程序登录失败", error); + reject(error); + }); + }); + }; + // 获取用户信息 const getInfo = () => { return new Promise((resolve, reject) => { @@ -59,7 +77,8 @@ export const useUserStore = defineStore("user", () => { } catch (error) { console.error("登出失败", error); } finally { - clearAll(); // 清除本地的 token 和用户信息缓存 + clearTokens(); // 清除本地的 token + Storage.remove(USER_INFO_KEY); // 清除用户信息缓存 userInfo.value = undefined; // 清空用户信息 } }; @@ -75,6 +94,7 @@ export const useUserStore = defineStore("user", () => { userInfo, login, loginByWechat, + loginByWechatMini, logout, getInfo, isUserInfoComplete, diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index 5fab0a3..3e3bdcc 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -15,6 +15,7 @@ declare global { const effectScope: typeof import('vue')['effectScope'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] + const guessSerializerType: typeof import('@uni-helper/uni-use')['guessSerializerType'] const h: typeof import('vue')['h'] const inject: typeof import('vue')['inject'] const isProxy: typeof import('vue')['isProxy'] @@ -76,17 +77,53 @@ declare global { const toRefs: typeof import('vue')['toRefs'] const toValue: typeof import('vue')['toValue'] const triggerRef: typeof import('vue')['triggerRef'] + const tryOnBackPress: typeof import('@uni-helper/uni-use')['tryOnBackPress'] + const tryOnHide: typeof import('@uni-helper/uni-use')['tryOnHide'] + const tryOnInit: typeof import('@uni-helper/uni-use')['tryOnInit'] + const tryOnLoad: typeof import('@uni-helper/uni-use')['tryOnLoad'] + const tryOnReady: typeof import('@uni-helper/uni-use')['tryOnReady'] + const tryOnScopeDispose: typeof import('@uni-helper/uni-use')['tryOnScopeDispose'] + const tryOnShow: typeof import('@uni-helper/uni-use')['tryOnShow'] + const tryOnUnload: typeof import('@uni-helper/uni-use')['tryOnUnload'] const unref: typeof import('vue')['unref'] + const useActionSheet: typeof import('@uni-helper/uni-use')['useActionSheet'] const useAttrs: typeof import('vue')['useAttrs'] + const useClipboardData: typeof import('@uni-helper/uni-use')['useClipboardData'] const useCssModule: typeof import('vue')['useCssModule'] const useCssVars: typeof import('vue')['useCssVars'] + const useDownloadFile: typeof import('@uni-helper/uni-use')['useDownloadFile'] + const useGlobalData: typeof import('@uni-helper/uni-use')['useGlobalData'] const useId: typeof import('vue')['useId'] + const useInterceptor: typeof import('@uni-helper/uni-use')['useInterceptor'] const useLink: (typeof import("vue-router"))["useLink"] + const useLoading: typeof import('@uni-helper/uni-use')['useLoading'] + const useModal: typeof import('@uni-helper/uni-use')['useModal'] const useModel: typeof import('vue')['useModel'] - const useRoute: (typeof import("vue-router"))["useRoute"] - const useRouter: (typeof import("vue-router"))["useRouter"] + const useNetwork: typeof import('@uni-helper/uni-use')['useNetwork'] + const useOnline: typeof import('@uni-helper/uni-use')['useOnline'] + const usePage: typeof import('@uni-helper/uni-use')['usePage'] + const usePageScroll: typeof import('@uni-helper/uni-use')['usePageScroll'] + const usePages: typeof import('@uni-helper/uni-use')['usePages'] + const usePreferredDark: typeof import('@uni-helper/uni-use')['usePreferredDark'] + const usePreferredLanguage: typeof import('@uni-helper/uni-use')['usePreferredLanguage'] + const usePrevPage: typeof import('@uni-helper/uni-use')['usePrevPage'] + const usePrevRoute: typeof import('@uni-helper/uni-use')['usePrevRoute'] + const useProvider: typeof import('@uni-helper/uni-use')['useProvider'] + const useRequest: typeof import('@uni-helper/uni-use')['useRequest'] + const useRoute: typeof import('@uni-helper/uni-use')['useRoute'] + const useRouter: typeof import('@uni-helper/uni-use')['useRouter'] + const useScanCode: typeof import('@uni-helper/uni-use')['useScanCode'] + const useScreenBrightness: typeof import('@uni-helper/uni-use')['useScreenBrightness'] + const useSelectorQuery: typeof import('@uni-helper/uni-use')['useSelectorQuery'] const useSlots: typeof import('vue')['useSlots'] + const useSocket: typeof import('@uni-helper/uni-use')['useSocket'] + const useStorage: typeof import('@uni-helper/uni-use')['useStorage'] + const useStorageAsync: typeof import('@uni-helper/uni-use')['useStorageAsync'] + const useStorageSync: typeof import('@uni-helper/uni-use')['useStorageSync'] const useTemplateRef: typeof import('vue')['useTemplateRef'] + const useToast: typeof import('@uni-helper/uni-use')['useToast'] + const useUploadFile: typeof import('@uni-helper/uni-use')['useUploadFile'] + const useVisible: typeof import('@uni-helper/uni-use')['useVisible'] const watch: typeof import('vue')['watch'] const watchEffect: typeof import('vue')['watchEffect'] const watchPostEffect: typeof import('vue')['watchPostEffect'] diff --git a/src/utils/auth.ts b/src/utils/auth.ts index dfbb479..f32aff6 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,4 +1,46 @@ import { useUserStore } from "@/store/modules/user"; +import { Storage } from "./storage"; +import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants"; + +/** + * 获取访问令牌 + * @returns 返回访问令牌,如果不存在则返回null + */ +export function getAccessToken(): string | null { + return Storage.get(ACCESS_TOKEN_KEY) || null; +} + +/** + * 设置访问令牌 + * @param token 访问令牌 + */ +export function setAccessToken(token: string): void { + Storage.set(ACCESS_TOKEN_KEY, token); +} + +/** + * 获取刷新令牌 + * @returns 返回刷新令牌,如果不存在则返回null + */ +export function getRefreshToken(): string | null { + return Storage.get(REFRESH_TOKEN_KEY) || null; +} + +/** + * 设置刷新令牌 + * @param token 刷新令牌 + */ +export function setRefreshToken(token: string): void { + Storage.set(REFRESH_TOKEN_KEY, token); +} + +/** + * 清除所有令牌 + */ +export function clearTokens(): void { + Storage.remove(ACCESS_TOKEN_KEY); + Storage.remove(REFRESH_TOKEN_KEY); +} /** * 检查用户登录状态,未登录则跳转到登录页面 diff --git a/src/utils/cache.ts b/src/utils/cache.ts deleted file mode 100644 index 69865ef..0000000 --- a/src/utils/cache.ts +++ /dev/null @@ -1,40 +0,0 @@ -const TOKEN_KEY = "app-token"; -const USER_INFO_KEY = "user-info"; - -// 设置 token -export function setToken(token: string) { - uni.setStorageSync(TOKEN_KEY, token); -} - -// 获取 token -export function getToken(): string { - return uni.getStorageSync(TOKEN_KEY) || ""; -} - -// 清除 token -export function clearToken() { - uni.removeStorageSync(TOKEN_KEY); -} - -// 设置用户信息 -export function setUserInfo(userInfo: any) { - uni.setStorageSync(USER_INFO_KEY, userInfo); -} - -// 获取用户信息 -export function getUserInfo(): any { - return uni.getStorageSync(USER_INFO_KEY) || null; -} - -// 清除用户信息 -export function clearUserInfo() { - uni.removeStorageSync(USER_INFO_KEY); -} - -// 清除所有缓存信息 -export function clearAll() { - clearToken(); - clearUserInfo(); - // 清除字典缓存 - uni.removeStorageSync("dict_cache"); -} diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts deleted file mode 100644 index 96db635..0000000 --- a/src/utils/colorUtils.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 颜色处理工具类 - */ - -/** - * 获取浅色版本的颜色 - * @param hexColor 十六进制颜色值 - * @param factor 调亮因子 (0-1) - * @returns 调亮后的颜色值 - */ -export function getLighterColor(hexColor: string, factor: number): string { - // 去掉#前缀 - const hex = hexColor.replace("#", ""); - - // 解析RGB值 - let r = parseInt(hex.substring(0, 2), 16); - let g = parseInt(hex.substring(2, 4), 16); - let b = parseInt(hex.substring(4, 6), 16); - - // 调亮颜色 - r = Math.min(255, Math.floor(r + (255 - r) * factor)); - g = Math.min(255, Math.floor(g + (255 - g) * factor)); - b = Math.min(255, Math.floor(b + (255 - b) * factor)); - - // 转回16进制 - return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; -} - -/** - * 获取深色版本的颜色 - * @param hexColor 十六进制颜色值 - * @param factor 调暗因子 (0-1) - * @returns 调暗后的颜色值 - */ -export function getDarkerColor(hexColor: string, factor: number): string { - // 去掉#前缀 - const hex = hexColor.replace("#", ""); - - // 解析RGB值 - let r = parseInt(hex.substring(0, 2), 16); - let g = parseInt(hex.substring(2, 4), 16); - let b = parseInt(hex.substring(4, 6), 16); - - // 调暗颜色 - r = Math.max(0, Math.floor(r * factor)); - g = Math.max(0, Math.floor(g * factor)); - b = Math.max(0, Math.floor(b * factor)); - - // 转回16进制 - return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; -} diff --git a/src/utils/request.ts b/src/utils/request.ts index 5909a49..a1e1d19 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,4 +1,4 @@ -import { getToken, clearAll } from "@/utils/cache"; +import { getAccessToken, clearTokens } from "@/utils/auth"; import { ResultCodeEnum } from "@/enums/ResultCodeEnum"; export default function request(options: UniApp.RequestOptions): Promise { @@ -14,7 +14,7 @@ export default function request(options: UniApp.RequestOptions): Promise { url: `${baseApi}${options.url}`, header: { ...options.header, - Authorization: getToken() ? `Bearer ${getToken()}` : "", + Authorization: getAccessToken() ? `Bearer ${getAccessToken()}` : "", }, success: (response) => { console.log("success response", response); @@ -27,7 +27,7 @@ export default function request(options: UniApp.RequestOptions): Promise { // 令牌失效或过期处理 else if (resData.code === ResultCodeEnum.TOKEN_INVALID) { console.log("令牌失效或过期处理"); - clearAll(); + clearTokens(); // 跳转到登录页 uni.reLaunch({ url: "/pages/login/index", diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..a39f65e --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,72 @@ +/** + * 存储工具类 + * 提供localStorage和sessionStorage操作方法 + */ + +/** + * localStorage 存储 + */ +function set(key: string, value: any): void { + uni.setStorageSync(key, JSON.stringify(value)); +} + +function get(key: string, defaultValue?: T): T { + const value = uni.getStorageSync(key); + if (!value) return defaultValue as T; + + try { + return JSON.parse(value); + } catch { + // 如果解析失败,返回原始字符串 + return value as unknown as T; + } +} + +function remove(key: string): void { + uni.removeStorageSync(key); +} + +export const Storage = { + set, + get, + remove, +}; + +// 为了向后兼容,导出具体的函数 +import { ACCESS_TOKEN_KEY, USER_INFO_KEY } from "@/constants"; + +/** + * 获取令牌 + */ +export function getToken(): string | null { + return Storage.get(ACCESS_TOKEN_KEY) || null; +} + +/** + * 设置令牌 + */ +export function setToken(token: string): void { + Storage.set(ACCESS_TOKEN_KEY, token); +} + +/** + * 获取用户信息 + */ +export function getUserInfo(): T | undefined { + return Storage.get(USER_INFO_KEY); +} + +/** + * 设置用户信息 + */ +export function setUserInfo(userInfo: any): void { + Storage.set(USER_INFO_KEY, userInfo); +} + +/** + * 清除所有数据 + */ +export function clearAll(): void { + Storage.remove(ACCESS_TOKEN_KEY); + Storage.remove(USER_INFO_KEY); +} diff --git a/uni-pages.d.ts b/uni-pages.d.ts new file mode 100644 index 0000000..03826df --- /dev/null +++ b/uni-pages.d.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by vite-plugin-uni-pages + +interface NavigateToOptions { + url: "/pages/index/index" | + "/pages/login/complete-profile" | + "/pages/login/index" | + "/pages/mine/index" | + "/pages/mine/about/index" | + "/pages/mine/faq/index" | + "/pages/mine/feedback/index" | + "/pages/mine/profile/index" | + "/pages/mine/settings/index" | + "/pages/mine/settings/account/index" | + "/pages/mine/settings/agreement/index" | + "/pages/mine/settings/network/index" | + "/pages/mine/settings/privacy/index" | + "/pages/mine/settings/theme/index"; +} +interface RedirectToOptions extends NavigateToOptions {} + +interface SwitchTabOptions { + +} + +type ReLaunchOptions = NavigateToOptions | SwitchTabOptions; + +declare interface Uni { + navigateTo(options: UniNamespace.NavigateToOptions & NavigateToOptions): void; + redirectTo(options: UniNamespace.RedirectToOptions & RedirectToOptions): void; + switchTab(options: UniNamespace.SwitchTabOptions & SwitchTabOptions): void; + reLaunch(options: UniNamespace.ReLaunchOptions & ReLaunchOptions): void; +} diff --git a/vite.config.ts b/vite.config.ts index 13591c1..dc0b2cc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,9 @@ import { defineConfig, type UserConfig, type ConfigEnv, loadEnv } from "vite"; import uni from "@dcloudio/vite-plugin-uni"; import AutoImport from "unplugin-auto-import/vite"; +import UniLayouts from "@uni-helper/vite-plugin-uni-layouts"; +import UniPages from "@uni-helper/vite-plugin-uni-pages"; +import { uniuseAutoImports } from "@uni-helper/uni-use"; export default defineConfig(async ({ mode }: ConfigEnv): Promise => { const UnoCss = await import("unocss/vite").then((i) => i.default); @@ -20,17 +23,26 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise => }, }, }, + build: { + target: "es6", + cssTarget: "chrome61", + }, + optimizeDeps: { + exclude: ["vue-demi"], + }, plugins: [ - // https://github.com/unocss/unocss + // 在 uni() 之前使用 UnoCss(), - + UniLayouts(), + UniPages(), AutoImport({ - imports: ["vue", "uni-app"], + imports: ["vue", "uni-app", uniuseAutoImports()], dts: "src/types/auto-imports.d.ts", // 自动生成的类型声明文件 eslintrc: { enabled: false, }, }), + uni(), ], };