From 65656f1810a36fe04135a1908dd4670beaefd76a Mon Sep 17 00:00:00 2001 From: yuantao Date: Wed, 5 Nov 2025 16:20:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 24 +- .eslintignore | 6 + .eslintrc.js | 56 +++++ .prettierignore | 6 + .prettierrc.js | 37 +++ App.vue | 1 + README.md | 239 ++++++++++++------ api/index.js | 15 ++ api/modules/user.js | 85 +++++++ api/request.js | 165 ++++++++++-- common/constants/app.js | 30 +++ common/styles/base.scss | 304 ++++++++++++++-------- common/styles/dark-mode.scss | 88 +++++++ common/utils/env.js | 80 ++++++ common/utils/tool.js | 388 +++++++++++++++++++++++++---- components/common/Card.vue | 74 ++++++ components/common/CustomButton.vue | 195 +++++++++++++++ components/common/FormInput.vue | 141 +++++++++++ directives/index.js | 141 +++++++++++ hooks/index.js | 7 + hooks/useApi.js | 157 ++++++++++++ hooks/useRequest.js | 113 +++++++++ hooks/useState.js | 140 +++++++++++ main.js | 8 +- package.json | 40 ++- uni.scss | 99 ++++++-- vite.config.js | 60 ++++- 27 files changed, 2407 insertions(+), 292 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 api/index.js create mode 100644 api/modules/user.js create mode 100644 common/constants/app.js create mode 100644 common/styles/dark-mode.scss create mode 100644 common/utils/env.js create mode 100644 components/common/Card.vue create mode 100644 components/common/CustomButton.vue create mode 100644 components/common/FormInput.vue create mode 100644 directives/index.js create mode 100644 hooks/index.js create mode 100644 hooks/useApi.js create mode 100644 hooks/useRequest.js create mode 100644 hooks/useState.js diff --git a/.env b/.env index 6b96cba..b3c75e4 100644 --- a/.env +++ b/.env @@ -1,4 +1,20 @@ -VITE_BASE_URL= #接口地址 -VITE_ASSETSURL=https://cdn.vrupup.com/s/1598/assets/ #资源地址 -VITE_APPID=wx9cb717d8151d8486 #小程序APPID -VITE_UNI_APPID=_UNI_8842336 #UNI-APPID \ No newline at end of file +# 接口地址 +VITE_BASE_URL= + +# 资源地址 +VITE_ASSETSURL=https://cdn.vrupup.com/s/1598/assets/ + +# 小程序APPID +VITE_APPID=wx9cb717d8151d8486 + +# UNI-APPID +VITE_UNI_APPID=_UNI_8842336 + +# 调试模式 +VITE_DEBUG=false + +# API超时时间(毫秒) +VITE_API_TIMEOUT=10000 + +# 版本号 +VITE_APP_VERSION=1.0.0 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..f26eb7d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +dist +.hbuilderx +unpackage +.env* +!.env.example \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..78fc279 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,56 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + es6: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:vue/vue3-essential', + 'prettier', + 'plugin:prettier/recommended', + ], + parserOptions: { + parser: '@babel/eslint-parser', + ecmaVersion: 2020, + sourceType: 'module', + }, + plugins: ['vue', 'prettier'], + rules: { + 'prettier/prettier': 'error', + 'vue/multi-word-component-names': 'off', + 'vue/no-unused-vars': 'error', + 'vue/html-self-closing': 'off', + 'vue/max-attributes-per-line': [ + 'error', + { + singleline: { + max: 3, + }, + multiline: { + max: 1, + }, + }, + ], + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-unused-vars': 'error', + 'no-undef': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + 'object-shorthand': 'error', + 'quote-props': ['error', 'as-needed'], + 'array-callback-return': 'error', + 'prefer-arrow-callback': 'error', + 'arrow-parens': ['error', 'as-needed'], + 'arrow-spacing': 'error', + 'no-duplicate-imports': 'error', + }, + globals: { + uni: 'readonly', + getApp: 'readonly', + getCurrentPages: 'readonly', + plus: 'readonly', + }, +} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f26eb7d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +.hbuilderx +unpackage +.env* +!.env.example \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..9c77283 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,37 @@ +module.exports = { + // 一行最多 100 字符 + printWidth: 100, + // 使用 2 个空格缩进 + tabWidth: 2, + // 不使用缩进符,而使用空格 + useTabs: false, + // 行尾需要有分号 + semi: false, + // 使用单引号 + singleQuote: true, + // 对象的 key 仅在必要时用引号 + quoteProps: 'as-needed', + // jsx 不使用单引号,而使用双引号 + jsxSingleQuote: false, + // 末尾不需要逗号 + trailingComma: 'none', + // 大括号内的首尾需要空格 + bracketSpacing: true, + // jsx 标签的反尖括号需要换行 + bracketSameLine: false, + // 箭头函数,只有一个参数的时候,也需要括号 + arrowParens: 'avoid', + // 每个文件格式化的范围是文件的全部内容 + rangeStart: 0, + rangeEnd: Infinity, + // 不需要写文件开头的 @prettier + requirePragma: false, + // 不需要自动在文件开头插入 @prettier + insertPragma: false, + // 使用默认的折行标准 + proseWrap: 'preserve', + // 根据显示样式决定 html 要不要折行 + htmlWhitespaceSensitivity: 'css', + // 换行符使用 lf + endOfLine: 'lf', +} \ No newline at end of file diff --git a/App.vue b/App.vue index 55794e0..ad2cf28 100644 --- a/App.vue +++ b/App.vue @@ -12,5 +12,6 @@ export default { @import "./uni.scss"; @import '@/common/styles/common.css'; @import '@/common/styles/base.scss'; +@import '@/common/styles/dark-mode.scss'; @import '@/uview-plus/index.scss'; diff --git a/README.md b/README.md index 216e107..c581331 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,159 @@ -# Ŀģʹ˵ - -ģǻ UniApp + Vue3 + uView-Plus СĿģ壬һЩõá͹ߺ - -## Ŀ¼ṹ - - ``` - api/ # ӿ - modules/ # ҵӿ - request.js # װ - common/ # Դ - styles/ # ȫʽ - common.css # codefunԭʽ - base.scss # ȫʽ - utils/ # ߺ - tool.ts # ùߺ - components/ # - uni_modules/ # uni-app - z-paging/ # ҳ - lib/ # - luch-request/ # luch-request - uview-plus/ # uView-Plus - mixins/ # Vue - global.ts # ȫֻ - store/ # ״̬ - index.ts # Vuex store - pages/ # ҳ - subPages/ # ְҳ - App.vue # Ӧ - main.js # ļ - pages.json # ҳ - manifest.json # Ӧ - uni.scss # ȫʽ - vite.config.js # Vite - .nvmdrc # Node.js 汾Ҫ - .env # -``` - -## ʹ÷ - -1. ģĿ¼ƵĿĿ¼ -2. Ҫ޸ package.json еĿƺ -3. ʹ npm install װ -4. Ҫ޸ pages.json еҳ -5. ʼ¹ - -## Ҫ - -### Ҫ -- **dotenv** - ע - -### ʽ - -- common.css: ȫֻʽ -- base.scss: õ SCSS - -### ߺ (tool.js) - -- alert: ʾ -- loading/hideLoading: ʾ/ؼʾ -- ҳתط: navigateTo, redirectTo, reLaunch, switchTab, navigateBack -- ػ: storage, removeStorage, getStorageInfo -- copy: ı -- saveImageToPhotos: ͼƬ -- requestPayment: ΢֧ -- upload: ļϴ - -### - -- App.vue: ȫʽͻ -- main.js: Vue Ӧóʼȫֲ -- pages.json: ҳ·ɺʹ -- uni.scss: ȫʽ - -## ע - -1. ʵĿ -2. Ŀ޸Ļչߺ -3. ɸҪ޸Ļ滻 -4. ʽļɸĿƹ淶е +# UniApp 微信小程序快速开发模板 + +本模板是基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板,提供了一些常用的工具函数、组件和最佳实践。 + +## 目录结构 + +``` +├── api/ # 接口相关 +│ ├── modules/ # 业务接口 +│ └── request.js # 请求封装 +├── common/ # 公共资源 +│ ├── styles/ # 全局样式 +│ │ ├── common.css # codefun原子类样式 +│ │ └── base.scss # 全局样式变量 +│ └── utils/ # 工具函数 +│ └── tool.js # 常用工具函数 +├── components/ # 公共组件 +├── uni_modules/ # uni-app 组件 +│ └── z-paging/ # 分页组件库 +├── lib/ # 第三方库 +│ └── luch-request/ # luch-request 网络请求库 +├── uview-plus/ # uView-Plus 组件库 +├── mixins/ # Vue 混入 +│ └── global.js # 全局混入 +├── store/ # 状态管理 +├── pages/ # 主包页面 +├── subPages/ # 分包页面 +├── App.vue # 应用入口 +├── main.js # 主入口文件 +├── pages.json # 页面配置 +├── manifest.json # 应用配置 +├── uni.scss # 全局样式变量 +├── vite.config.js # Vite 编译配置 +├── .nvmdrc # Node.js 版本要求 +└── .env # 环境变量 +``` + +## 使用方法 + +1. 将模板目录复制到你的项目目录中 +2. 根据需要修改 package.json 中的项目名称和描述 +3. 使用 npm install 安装依赖 +4. 根据需要修改 pages.json 中的页面配置 +5. 开始构建你的新项目 + +## 开发环境与运行 + +### 环境要求 + +* Node.js (版本信息在 `.nvmdrc` 文件中指定,当前为 20.0.0) +* npm 或 yarn + +### 安装依赖 + +```bash +npm install +``` + +### 运行项目 + +由于这是一个 UniApp 项目,通常需要使用 HBuilderX 或其他支持 UniApp 的 IDE 来运行和调试。具体的运行命令取决于你的开发环境。 + +### 构建项目 + +同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具。 + +## 项目特性 + +### 核心依赖 +- **dotenv** - 环境变量注入 + +### 样式系统 + +- common.css: 全局原子类样式 +- base.scss: 实用的 SCSS 工具类 + +### 工具函数 (tool.js) + +- alert: 文字轻提示 +- loading/hideLoading: 显示/隐藏加载提示 +- 页面跳转方法: navigateTo, redirectTo, reLaunch, switchTab, navigateBack +- 本地存储: storage, removeStorage, getStorageInfo +- copy: 复制文本到剪贴板 +- saveImageToPhotos: 保存图片到相册 +- requestPayment: 微信支付 +- upload: 文件上传 +- formatDate: 日期格式化 +- deepClone: 深拷贝 +- debounce/throttle: 防抖节流 +- getValidator: 表单验证工具 +- toggleDarkMode/getCurrentTheme: 暗黑模式切换 + +### 网络请求 + +- 基于 luch-request 的封装 +- 自动添加 token +- 统一错误处理 +- 请求缓存机制 +- 请求重试机制 + +### 组件 + +- uView-Plus 组件库 +- z-paging 分页组件 + +## 代码规范与开发约定 + +### 样式 + +- 全局样式文件位于 `common/styles/` 目录下 +- 遵循项目中已有的风格 + +### JavaScript + +- 严格遵循ES6规范 +- 遵循JavaScript函数式编程范式 +- 方法类函数应该使用 `function` 进行定义 +- 页面的生命周期需要通过 `@dcloudio/uni-app` 依赖进行按需导入 +- 全局变量都集中放置于代码顶部 +- 变量名使用小驼峰命名法 +- 常量名使用全大写 +- 所有 `Promise` 类方法使用 `async` `await` 写法 +- 在需要页面跳转、提示、加载、本地存储等功能的时候,优先使用工具函数 +- 字符串拼接使用ES6的模板语法 + +### 静态资源 + +- 静态资源变量 `ASSETSURL` 已进行全局混入,可以在 `` 中直接使用 + +### 网络请求 + +- 网络请求使用 `lib/luch-request` 库进行封装 +- 包含请求和响应拦截器,用于处理通用逻辑 +- 各业务板块的接口都应存放在 `api/modules` 下 + +### 组件 + +- 所有 `uni_modules` 目录中的组件无需导入直接可以进行使用 +- `uView-Plus` 组件已通过 `easycom` 自动导入 + +### 页面 + +- 页面配置在 `pages.json` 中管理 +- 页面使用 Composition API (setup语法糖) 编写 + +## 代码提交规范 + +- 提交信息应清晰描述变更内容 +- 对于功能性的新增或修改,使用 `新增` 前缀 +- 对于错误修复,使用 `修复` 前缀 +- 对于性能优化、代码重构,使用 `优化` 前缀 +- 对于文档更新,使用 `文档` 前缀 +- 提交信息应使用中文 + +## 注意事项 + +1. 请根据实际项目需求进行修改和扩展功能 +2. 代码文件可按项目规范进行修改替换 +3. 样式文件可按项目规范进行修改 \ No newline at end of file diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..77ba1bc --- /dev/null +++ b/api/index.js @@ -0,0 +1,15 @@ +/** + * API统一导出入口 + */ +// 导入网络请求实例 +import http from './request.js' +// 导入各业务模块 +import user from './modules/user.js' +// 导出网络请求实例 +export { http } +// 导出各业务模块 +export { user } +// 默认导出包含所有模块的对象 +export default { + user, +} diff --git a/api/modules/user.js b/api/modules/user.js new file mode 100644 index 0000000..60739f9 --- /dev/null +++ b/api/modules/user.js @@ -0,0 +1,85 @@ +import http from '@/api/request.js' + +/** + * 用户相关API模块 + */ +export default { + /** + * 用户登录 + * @param {Object} data 登录数据 + * @returns {Promise} + */ + login(data) { + return http.post('/user/login', data) + }, + + /** + * 用户注册 + * @param {Object} data 注册数据 + * @returns {Promise} + */ + register(data) { + return http.post('/user/register', data) + }, + + /** + * 获取用户信息 + * @returns {Promise} + */ + getUserInfo() { + return http.get('/user/info') + }, + + /** + * 更新用户信息 + * @param {Object} data 用户信息数据 + * @returns {Promise} + */ + updateUserInfo(data) { + return http.put('/user/info', data) + }, + + /** + * 修改密码 + * @param {Object} data 密码数据 + * @returns {Promise} + */ + changePassword(data) { + return http.post('/user/change-password', data) + }, + + /** + * 退出登录 + * @returns {Promise} + */ + logout() { + return http.post('/user/logout') + }, + + /** + * 获取用户列表 + * @param {Object} params 查询参数 + * @returns {Promise} + */ + getUserList(params) { + return http.get('/user/list', { params }) + }, + + /** + * 获取用户详情 + * @param {number} userId 用户ID + * @returns {Promise} + */ + getUserDetail(userId) { + return http.get(`/user/${userId}`) + }, + + /** + * 删除用户 + * @param {number} userId 用户ID + * @returns {Promise} + */ + deleteUser(userId) { + return http.delete(`/user/${userId}`) + }, +} diff --git a/api/request.js b/api/request.js index 3174fdc..250c6e0 100644 --- a/api/request.js +++ b/api/request.js @@ -1,9 +1,46 @@ -import Request from '@/lib/luch-request/index.js' -import tool from '@/common/utils/tool.js' - -const baseUrl = import.meta.env.VITE_BASE_URL +import Request from '@/lib/luch-request/index.js' +import tool from '@/common/utils/tool.js' +import EnvConfig from '@/common/utils/env.js' + +const baseUrl = EnvConfig.BASE_URL const http = new Request() - +// 缓存存储 +const cache = new Map() +// 缓存过期时间(毫秒),默认5分钟 +const CACHE_EXPIRATION = 5 * 60 * 1000 +// 缓存工具方法 +const cacheUtils = { + // 生成缓存key + generateKey(config) { + return `${config.method}:${config.url}:${JSON.stringify(config.data || {})}:${JSON.stringify(config.params || {})}` + }, + // 获取缓存 + get(key) { + const cached = cache.get(key) + if (!cached) return null + // 检查是否过期 + if (Date.now() - cached.timestamp > CACHE_EXPIRATION) { + cache.delete(key) + return null + } + return cached.data + }, + // 设置缓存 + set(key, data) { + cache.set(key, { + data, + timestamp: Date.now(), + }) + }, + // 清除缓存 + clear() { + cache.clear() + }, + // 清除指定缓存 + delete(key) { + cache.delete(key) + }, +} /* 设置全局配置 */ http.setConfig(config => { config.header = { ...config.header } @@ -11,31 +48,115 @@ http.setConfig(config => { config.baseURL = baseUrl return config }) - http.interceptors.request.use( async config => { config.header = { ...config.header } + // 自动添加token + const token = uni.getStorageSync('token') + if (token) { + config.header.Authorization = `Bearer ${token}` + } + // 添加通用请求头 + config.header['Content-Type'] = 'application/json' + // 检查缓存 + if (config.method === 'GET' && config.cache !== false) { + const cacheKey = cacheUtils.generateKey(config) + const cachedData = cacheUtils.get(cacheKey) + if (cachedData) { + // 如果有缓存,直接返回缓存数据 + return Promise.resolve({ + data: cachedData, + statusCode: 200, + }) + } + } return config }, config => { return Promise.reject(config) } ) - -http.interceptors.response.use(response => { - if (response.statusCode == 500 || response.statusCode == 404 || response.statusCode == 403) { - console.error(response) - return tool.alert('网络错误,请稍后重试') +http.interceptors.response.use( + response => { + // 网络错误处理 + if (response.statusCode >= 500) { + console.error('服务器错误:', response) + tool.alert('服务器错误,请稍后重试') + return Promise.reject(response) + } + if (response.statusCode === 404) { + console.error('接口不存在:', response) + tool.alert('接口不存在') + return Promise.reject(response) + } + if (response.statusCode === 403) { + console.error('无权限访问:', response) + tool.alert('无权限访问') + return Promise.reject(response) + } + // 未授权处理 + if (response.statusCode === 401 || response.data?.code === 401) { + console.error('未授权或登录过期:', response) + tool.alert('登录已过期,请重新登录') + // 清除本地token + uni.removeStorageSync('token') + // 跳转到登录页 + setTimeout(() => { + uni.reLaunch({ url: '/pages/login/login' }) // 根据实际登录页路径调整 + }, 1500) + return Promise.reject(response) + } + // 业务错误处理 + if (response.data && response.data.code !== undefined && response.data.code !== 200) { + const message = response.data.message || response.data.msg || '业务错误' + console.error('业务错误:', message) + tool.alert(message) + return Promise.reject(response) + } + // 成功响应 + if (response.statusCode === 200) { + // 缓存GET请求的响应数据 + if (response.config && response.config.method === 'GET' && response.config.cache !== false) { + const cacheKey = cacheUtils.generateKey(response.config) + cacheUtils.set(cacheKey, response.data) + } + return Promise.resolve(response) + } else { + return Promise.reject(response) + } + }, + error => { + console.error('请求失败:', error) + if (error.errMsg) { + // 网络错误 + tool.alert('网络连接失败,请检查网络') + } else { + tool.alert('请求失败,请稍后重试') + } + return Promise.reject(error) } - - if (response.statusCode == 401 || response.data.code == 401) { - } - - if (response.statusCode == 200) { - return Promise.resolve(response) - } else { - return Promise.reject(response) - } -}) - +) +// 添加缓存功能到http实例 +http.cache = cacheUtils +// 添加重试方法 +http.retry = async function (config, retries = 3, delay = 1000) { + return new Promise((resolve, reject) => { + const attempt = async retryCount => { + try { + const response = await this.request(config) + resolve(response) + } catch (error) { + if (retryCount <= 0) { + reject(error) + } else { + // 延迟后重试 + setTimeout(() => { + attempt(retryCount - 1) + }, delay) + } + } + } + attempt(retries) + }) +} export default http diff --git a/common/constants/app.js b/common/constants/app.js new file mode 100644 index 0000000..df078ea --- /dev/null +++ b/common/constants/app.js @@ -0,0 +1,30 @@ +/** + * 应用相关常量 + */ + +// 应用信息 +export const APP_NAME = '小程序模板' +export const APP_VERSION = '1.0.0' + +// 页面路径常量 +export const PAGE_PATHS = { + INDEX: '/pages/index/index', + WELCOME: '/subPages/welcome/index', + LOGIN: '/pages/login/login', // 示例路径 + REGISTER: '/pages/register/register' // 示例路径 +} + +// 存储键名常量 +export const STORAGE_KEYS = { + TOKEN: 'token', + USER_INFO: 'userInfo', + THEME: 'theme', + LANG: 'language' +} + +// 事件常量 +export const EVENTS = { + USER_LOGIN: 'userLogin', + USER_LOGOUT: 'userLogout', + THEME_CHANGE: 'themeChange' +} \ No newline at end of file diff --git a/common/styles/base.scss b/common/styles/base.scss index f605867..a2f6da4 100644 --- a/common/styles/base.scss +++ b/common/styles/base.scss @@ -1,118 +1,97 @@ -// 定义内外边距,历遍1-100 -@for $i from 0 through 100 { - // 只要双数和能被5除尽的数 - @if $i % 2==0 or $i % 5==0 { - // 得出:u-margin-30或者u-m-30 - .w-#{$i} { - width: calc($i * 1%) !important; - } - .p-x-#{$i} { - padding-left: $i + rpx !important; - padding-right: $i + rpx !important; - } - - .p-y-#{$i} { - padding-top: $i + rpx !important; - padding-bottom: $i + rpx !important; - } - - .m-x-#{$i} { - margin-left: $i + rpx !important; - margin-right: $i + rpx !important; - } - - .m-y-#{$i} { - margin-top: $i + rpx !important; - margin-bottom: $i + rpx !important; - } - - .m-#{$i} { - margin-left: $i + rpx !important; - margin-right: $i + rpx !important; - margin-top: $i + rpx !important; - margin-bottom: $i + rpx !important; - } - - .p-#{$i} { - padding-left: $i + rpx !important; - padding-right: $i + rpx !important; - padding-top: $i + rpx !important; - padding-bottom: $i + rpx !important; - } - - .m-l-#{$i} { - margin-left: $i + rpx !important; - } - - .m-t-#{$i} { - margin-top: $i + rpx !important; - } - - .m-r-#{$i} { - margin-right: $i + rpx !important; - } - - .m-b-#{$i} { - margin-bottom: $i + rpx !important; - } - - .p-l-#{$i} { - padding-left: $i + rpx !important; - } - - .p-t-#{$i} { - padding-top: $i + rpx !important; - } - - .p-r-#{$i} { - padding-right: $i + rpx !important; - } - - .p-b-#{$i} { - padding-bottom: $i + rpx !important; - } - - .l-p-#{$i} { - letter-spacing: $i + rpx !important; - } - - .z-i-#{$i} { - z-index: $i; - } - - .l-h-#{$i} { - line-height: $i + rpx !important; - } - .r-#{$i} { - right: $i + rpx !important; - } - .l-#{$i} { - left: $i + rpx !important; - } - - .t-#{$i} { - top: $i + rpx !important; - } - .b-#{$i} { - bottom: $i + rpx !important; - } +// 定义内外边距,只生成常用的数值 +$spacing-sizes: (0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100); +@each $i in $spacing-sizes { + .w-#{$i} { + width: calc($i * 1%) !important; + } + .p-x-#{$i} { + padding-left: $i + rpx !important; + padding-right: $i + rpx !important; + } + .p-y-#{$i} { + padding-top: $i + rpx !important; + padding-bottom: $i + rpx !important; + } + .m-x-#{$i} { + margin-left: $i + rpx !important; + margin-right: $i + rpx !important; + } + .m-y-#{$i} { + margin-top: $i + rpx !important; + margin-bottom: $i + rpx !important; + } + .m-#{$i} { + margin-left: $i + rpx !important; + margin-right: $i + rpx !important; + margin-top: $i + rpx !important; + margin-bottom: $i + rpx !important; + } + .p-#{$i} { + padding-left: $i + rpx !important; + padding-right: $i + rpx !important; + padding-top: $i + rpx !important; + padding-bottom: $i + rpx !important; + } + .m-l-#{$i} { + margin-left: $i + rpx !important; + } + .m-t-#{$i} { + margin-top: $i + rpx !important; + } + .m-r-#{$i} { + margin-right: $i + rpx !important; + } + .m-b-#{$i} { + margin-bottom: $i + rpx !important; + } + .p-l-#{$i} { + padding-left: $i + rpx !important; + } + .p-t-#{$i} { + padding-top: $i + rpx !important; + } + .p-r-#{$i} { + padding-right: $i + rpx !important; + } + .p-b-#{$i} { + padding-bottom: $i + rpx !important; + } + .l-p-#{$i} { + letter-spacing: $i + rpx !important; + } + .z-i-#{$i} { + z-index: $i; + } + .l-h-#{$i} { + line-height: $i + rpx !important; + } + .r-#{$i} { + right: $i + rpx !important; + } + .l-#{$i} { + left: $i + rpx !important; + } + .t-#{$i} { + top: $i + rpx !important; + } + .b-#{$i} { + bottom: $i + rpx !important; } } - -// 定义字体(rpx)单位,大于或等于20的都为rpx单位字体 -@for $i from 9 through 60 { +// 定义字体(rpx)单位,只生成常用的字体大小 +$font-sizes: (9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 40, 44, 48, 52, 56, 60); +@each $i in $font-sizes { .font-#{$i} { font-size: $i + rpx !important; } } - // 圆角 -@for $i from 4 through 60 { +$rounded-sizes: (4, 6, 8, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60); +@each $i in $rounded-sizes { .rounded-#{$i} { border-radius: $i + rpx; } } - // 多行文本溢出 @for $i from 1 through 5 { .over-line-#{$i} { @@ -125,3 +104,120 @@ -webkit-box-orient: vertical; } } +// 显示相关工具类 +.display-none { + display: none !important; +} +.display-block { + display: block !important; +} +.display-inline { + display: inline !important; +} +.display-inline-block { + display: inline-block !important; +} +.display-flex { + display: flex !important; +} +// 浮动相关 +.float-left { + float: left !important; +} +.float-right { + float: right !important; +} +.float-none { + float: none !important; +} +.clearfix::after { + content: ''; + display: table; + clear: both; +} +// 文本对齐 +.text-left { + text-align: left !important; +} +.text-center { + text-align: center !important; +} +.text-right { + text-align: right !important; +} +.text-justify { + text-align: justify !important; +} +// 文本装饰 +.text-underline { + text-decoration: underline !important; +} +.text-line-through { + text-decoration: line-through !important; +} +.text-no-underline { + text-decoration: none !important; +} +// 字体粗细 +.font-normal { + font-weight: normal !important; +} +.font-bold { + font-weight: bold !important; +} +// 文本大小写 +.text-uppercase { + text-transform: uppercase !important; +} +.text-lowercase { + text-transform: lowercase !important; +} +.text-capitalize { + text-transform: capitalize !important; +} +// 垂直对齐 +.align-baseline { + vertical-align: baseline !important; +} +.align-top { + vertical-align: top !important; +} +.align-middle { + vertical-align: middle !important; +} +.align-bottom { + vertical-align: bottom !important; +} +// 位置相关 +.position-relative { + position: relative !important; +} +.position-absolute { + position: absolute !important; +} +.position-fixed { + position: fixed !important; +} +.position-static { + position: static !important; +} +// 溢出处理 +.overflow-auto { + overflow: auto !important; +} +.overflow-hidden { + overflow: hidden !important; +} +.overflow-visible { + overflow: visible !important; +} +.overflow-scroll { + overflow: scroll !important; +} +// 可见性 +.visible { + visibility: visible !important; +} +.invisible { + visibility: hidden !important; +} diff --git a/common/styles/dark-mode.scss b/common/styles/dark-mode.scss new file mode 100644 index 0000000..dadf739 --- /dev/null +++ b/common/styles/dark-mode.scss @@ -0,0 +1,88 @@ +/* 暗黑模式样式 */ +:root { + // 默认使用亮色模式变量 + --uni-color-primary: #007aff; + --uni-color-success: #4cd964; + --uni-color-warning: #f0ad4e; + --uni-color-error: #dd524d; + + --uni-text-color: #333; + --uni-text-color-inverse: #fff; + --uni-text-color-grey: #999; + --uni-text-color-placeholder: #808080; + --uni-text-color-disable: #c0c0c0; + + --uni-bg-color: #ffffff; + --uni-bg-color-grey: #f8f8f8; + --uni-bg-color-hover: #f1f1f1; + --uni-bg-color-mask: rgba(0, 0, 0, 0.4); + + --uni-border-color: #c8c7cc; +} + +/* 暗黑模式 */ +[data-theme='dark'] { + --uni-color-primary: #0a84ff; + --uni-color-success: #34c759; + --uni-color-warning: #ff9500; + --uni-color-error: #ff3b30; + + --uni-text-color: #f2f2f7; + --uni-text-color-inverse: #000000; + --uni-text-color-grey: #8e8e93; + --uni-text-color-placeholder: #4c4c4c; + --uni-text-color-disable: #636366; + + --uni-bg-color: #1c1c1e; + --uni-bg-color-grey: #2c2c2e; + --uni-bg-color-hover: #3a3a3c; + --uni-bg-color-mask: rgba(0, 0, 0, 0.6); + + --uni-border-color: #434346; +} + +/* 系统级暗黑模式 */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --uni-color-primary: #0a84ff; + --uni-color-success: #34c759; + --uni-color-warning: #ff9500; + --uni-color-error: #ff3b30; + + --uni-text-color: #f2f2f7; + --uni-text-color-inverse: #000000; + --uni-text-color-grey: #8e8e93; + --uni-text-color-placeholder: #4c4c4c; + --uni-text-color-disable: #636366; + + --uni-bg-color: #1c1c1e; + --uni-bg-color-grey: #2c2c2e; + --uni-bg-color-hover: #3a3a3c; + --uni-bg-color-mask: rgba(0, 0, 0, 0.6); + + --uni-border-color: #434346; + } +} + +/* 应用暗黑模式的通用类 */ +.dark-mode { + background-color: var(--uni-bg-color); + color: var(--uni-text-color); +} + +.dark-mode .dark-invert { + background-color: var(--uni-bg-color-inverse); + color: var(--uni-text-color-inverse); +} + +/* 通用暗黑模式切换类 */ +.light-mode { + background-color: var(--uni-bg-color); + color: var(--uni-text-color); +} + +/* 通用暗黑模式切换类 */ +.dark-mode { + background-color: var(--uni-bg-color); + color: var(--uni-text-color); +} diff --git a/common/utils/env.js b/common/utils/env.js new file mode 100644 index 0000000..5e83101 --- /dev/null +++ b/common/utils/env.js @@ -0,0 +1,80 @@ +/** + * 环境变量验证工具 + */ +/** + * 验证必需的环境变量 + * @param {Array} requiredVars 必需的环境变量键名数组 + * @throws {Error} 如果有任何必需的环境变量缺失则抛出错误 + */ +export function validateEnv(requiredVars = []) { + const missingVars = [] + for (const key of requiredVars) { + if (!import.meta.env[key]) { + missingVars.push(key) + } + } + if (missingVars.length > 0) { + throw new Error(`缺少必需的环境变量: ${missingVars.join(', ')}`) + } +} +/** + * 获取环境变量,带默认值和类型转换 + * @param {string} key 环境变量键名 + * @param {any} defaultValue 默认值 + * @param {string} type 类型 ('string' | 'number' | 'boolean' | 'array') + * @returns {any} 环境变量值 + */ +export function getEnv(key, defaultValue = null, type = 'string') { + const value = import.meta.env[key] + if (value === undefined || value === null || value === '') { + return defaultValue + } + switch (type) { + case 'number': + const numValue = Number(value) + return isNaN(numValue) ? defaultValue : numValue + case 'boolean': + return value === 'true' || value === '1' + case 'array': + try { + return JSON.parse(value) + } catch (e) { + return Array.isArray(defaultValue) ? defaultValue : [] + } + case 'string': + default: + return value + } +} +/** + * 环境变量配置 + */ +export const EnvConfig = { + // API基础URL + BASE_URL: getEnv('VITE_BASE_URL', '', 'string'), + // 静态资源URL + ASSETS_URL: getEnv('VITE_ASSETSURL', '', 'string'), + // 小程序APPID + APP_ID: getEnv('VITE_APPID', '', 'string'), + // UNI-APPID + UNI_APP_ID: getEnv('VITE_UNI_APPID', '', 'string'), + // 是否为开发环境 + IS_DEV: getEnv('NODE_ENV', 'development', 'string') === 'development', + // 是否为生产环境 + IS_PROD: getEnv('NODE_ENV', 'development', 'string') === 'production', + // 调试模式 + DEBUG: getEnv('VITE_DEBUG', false, 'boolean'), + // API超时时间(毫秒) + API_TIMEOUT: getEnv('VITE_API_TIMEOUT', 10000, 'number'), +} +// 验证必需的环境变量 +try { + validateEnv(['VITE_BASE_URL', 'VITE_APPID']) +} catch (error) { + console.error('环境变量验证失败:', error.message) + // 在开发环境中抛出错误 + if (EnvConfig.IS_DEV) { + throw error + } +} +export default EnvConfig \ No newline at end of file diff --git a/common/utils/tool.js b/common/utils/tool.js index 0f0395b..bcc650e 100644 --- a/common/utils/tool.js +++ b/common/utils/tool.js @@ -1,6 +1,5 @@ const baseUrl = import.meta.env.VITE_BASE_URL const assetsUrl = import.meta.env.VITE_ASSETSURL - /** * 工具类 - 提供常用的工具方法 * @class Tool @@ -13,11 +12,9 @@ class Tool { SUCCESS: 1, LOADING: 2, } - // 字体加载状态缓存 this.loadedFonts = new Set() } - /** * 文字轻提示 * @param {string} str 提示文字 @@ -30,13 +27,11 @@ class Tool { console.warn('alert方法需要提供提示文字') return } - const iconMap = { [this.ICON_TYPES.NONE]: 'none', [this.ICON_TYPES.SUCCESS]: 'success', [this.ICON_TYPES.LOADING]: 'loading', } - uni.showToast({ title: String(str), icon: iconMap[icon] || 'none', @@ -49,7 +44,6 @@ class Tool { }) }) } - /** * 显示loading加载 * @param {string} [title=' '] 加载文案 @@ -58,14 +52,12 @@ class Tool { loading(title = ' ', mask = true) { uni.showLoading({ title, mask }) } - /** * 关闭loading提示框 */ hideLoading() { uni.hideLoading() } - /** * 统一处理URL格式,确保以/开头 * @param {string} url 页面地址 @@ -76,17 +68,14 @@ class Tool { if (!url || typeof url !== 'string') { throw new Error('URL必须是字符串') } - return url.startsWith('/') ? url : `/${url}` } - /** * 可返回跳转(导航到新页面) * @param {string} url 页面地址 */ navigateTo(url) { const formattedUrl = this._formatUrl(url) - uni.navigateTo({ url: formattedUrl, fail: err => { @@ -95,7 +84,6 @@ class Tool { }, }) } - /** * 不可返回跳转(重定向到新页面) * @param {string} url 页面地址 @@ -103,7 +91,6 @@ class Tool { redirectTo(url) { uni.redirectTo({ url: this._formatUrl(url) }) } - /** * 清除页面栈跳转(重新启动到新页面) * @param {string} url 页面地址 @@ -111,7 +98,6 @@ class Tool { reLaunch(url) { uni.reLaunch({ url: this._formatUrl(url) }) } - /** * 跳转tabBar页 * @param {string} url 页面地址 @@ -119,7 +105,6 @@ class Tool { switchTab(url) { uni.switchTab({ url: this._formatUrl(url) }) } - /** * 返回上一页面或指定页面 * @param {number} [delta=1] 返回的页面数 @@ -127,7 +112,6 @@ class Tool { */ navigateBack(delta = 1, fallbackUrl = '/pages/index/index') { const pages = getCurrentPages() - if (pages.length <= 1) { console.warn('无上一页,使用回退地址') uni.reLaunch({ url: fallbackUrl }) @@ -135,7 +119,6 @@ class Tool { uni.navigateBack({ delta }) } } - /** * 操作本地缓存 * @param {string} key 缓存键值 @@ -146,24 +129,20 @@ class Tool { if (typeof key !== 'string') { throw new Error('key必须是字符串') } - // 设置操作 if (value !== undefined && value !== null) { uni.setStorageSync(key, value) return } - // 读取操作 if (key !== '#') { return uni.getStorageSync(key) } - // 特殊操作 if (key === '#') { uni.clearStorageSync() } } - /** * 删除指定缓存 * @param {string} key 要删除的缓存键 @@ -172,10 +151,8 @@ class Tool { if (typeof key !== 'string') { throw new Error('key必须是字符串') } - uni.removeStorageSync(key) } - /** * 获取缓存信息 * @returns {Object} 缓存信息 @@ -183,7 +160,6 @@ class Tool { getStorageInfo() { return uni.getStorageInfoSync() } - /** * 复制文本到剪贴板 * @param {string} data 要复制的文本 @@ -194,7 +170,6 @@ class Tool { this.alert('暂无内容') return false } - try { await new Promise((resolve, reject) => { uni.setClipboardData({ @@ -203,7 +178,6 @@ class Tool { fail: reject, }) }) - this.alert('复制成功') return true } catch (error) { @@ -212,7 +186,6 @@ class Tool { return false } } - /** * 导入外部字体 * @param {string} fontName 字体文件名(不含路径) @@ -222,15 +195,12 @@ class Tool { if (!fontName || typeof fontName !== 'string') { throw new Error('字体名称必须是字符串') } - // 检查是否已加载过 if (this.loadedFonts.has(fontName)) { return true } - try { const fontFamily = fontName.replace(/\.[^/.]+$/, '') // 移除文件扩展名 - await new Promise((resolve, reject) => { uni.loadFontFace({ family: fontFamily, @@ -240,7 +210,6 @@ class Tool { fail: reject, }) }) - this.loadedFonts.add(fontName) return true } catch (error) { @@ -248,7 +217,6 @@ class Tool { return false } } - /** * 保存图片到相册 * @param {string} url 图片URL @@ -259,7 +227,6 @@ class Tool { this.alert('图片地址不能为空') return false } - try { // 检查权限 const { authSetting } = await new Promise((resolve, reject) => { @@ -268,7 +235,6 @@ class Tool { fail: reject, }) }) - if (!authSetting['scope.writePhotosAlbum']) { // 请求权限 await new Promise((resolve, reject) => { @@ -279,7 +245,6 @@ class Tool { }) }) } - // 获取图片信息 const { path } = await new Promise((resolve, reject) => { uni.getImageInfo({ @@ -288,7 +253,6 @@ class Tool { fail: reject, }) }) - // 保存到相册 await new Promise((resolve, reject) => { uni.saveImageToPhotosAlbum({ @@ -297,12 +261,10 @@ class Tool { fail: reject, }) }) - this.alert('已保存到相册') return true } catch (error) { console.error('保存图片失败:', error) - if (error.errMsg && error.errMsg.includes('auth')) { // 权限相关错误 await new Promise(resolve => { @@ -313,16 +275,240 @@ class Tool { success: resolve, }) }) - uni.openSetting() } else { this.alert('保存失败,请重试') } - return false } } - + /** + * 防抖函数 + * @param {Function} func 要防抖的函数 + * @param {number} wait 延迟时间(ms) + * @param {boolean} immediate 是否立即执行 + * @returns {Function} 防抖后的函数 + */ + debounce(func, wait, immediate = false) { + let timeout + return function (...args) { + const later = () => { + timeout = null + if (!immediate) func.apply(this, args) + } + const callNow = immediate && !timeout + clearTimeout(timeout) + timeout = setTimeout(later, wait) + if (callNow) func.apply(this, args) + } + } + /** + * 节流函数 + * @param {Function} func 要节流的函数 + * @param {number} wait 延迟时间(ms) + * @returns {Function} 节流后的函数 + */ + throttle(func, wait) { + let timeout + let previous = 0 + return function (...args) { + const now = Date.now() + const remaining = wait - (now - previous) + const context = this + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + previous = now + func.apply(context, args) + } else if (!timeout) { + timeout = setTimeout(() => { + previous = Date.now() + timeout = null + func.apply(context, args) + }, remaining) + } + } + } + /** + * 日期格式化 + * @param {Date|string|number} date 日期对象、字符串或时间戳 + * @param {string} fmt 格式字符串, 默认为 'YYYY-MM-DD HH:mm:ss' + * @returns {string} 格式化后的日期字符串 + */ + formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') { + // 如果传入的是时间戳,转换为Date对象 + if (typeof date === 'number') { + date = new Date(date) + } else if (typeof date === 'string') { + // 如果传入的是字符串,尝试转换为Date对象 + date = new Date(date) + } + if (!(date instanceof Date) || isNaN(date.getTime())) { + throw new Error('Invalid date') + } + const o = { + 'Y+': date.getFullYear(), + 'M+': date.getMonth() + 1, + 'D+': date.getDate(), + 'H+': date.getHours(), + 'm+': date.getMinutes(), + 's+': date.getSeconds(), + 'q+': Math.floor((date.getMonth() + 3) / 3), + S: date.getMilliseconds(), + } + for (let k in o) { + if (new RegExp('(' + k + ')').test(fmt)) { + let str = o[k] + '' + if (k === 'S') { + fmt = fmt.replace(RegExp.$1, str.padStart(3, '0')) + } else { + fmt = fmt.replace(RegExp.$1, str.length === 1 ? '0' + str : str) + } + } + } + return fmt + } + /** + * 深拷贝函数 + * @param {*} obj 要深拷贝的对象 + * @returns {*} 拷贝后的对象 + */ + deepClone(obj) { + // 处理null、undefined和原始类型 + if (obj === null || typeof obj !== 'object') { + return obj + } + // 处理日期对象 + if (obj instanceof Date) { + return new Date(obj.getTime()) + } + // 处理数组和对象 + if (obj instanceof Array) { + return obj.map(item => this.deepClone(item)) + } + // 处理普通对象 + const clonedObj = {} + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + clonedObj[key] = this.deepClone(obj[key]) + } + } + return clonedObj + } + /** + * 表单验证工具 + * @class Validator + */ + getValidator() { + const rules = { + /** + * 必填验证 + * @param {*} value 值 + * @param {boolean} required 是否必填 + * @returns {boolean} 验证结果 + */ + required(value, required = true) { + if (!required) return true + return value !== undefined && value !== null && value !== '' + }, + /** + * 邮箱验证 + * @param {string} value 值 + * @returns {boolean} 验证结果 + */ + email(value) { + if (!value) return true + const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + return reg.test(value) + }, + /** + * 手机号验证 + * @param {string} value 值 + * @returns {boolean} 验证结果 + */ + mobile(value) { + if (!value) return true + const reg = /^1[3-9]\d{9}$/ + return reg.test(value) + }, + /** + * 最小长度验证 + * @param {string} value 值 + * @param {number} length 最小长度 + * @returns {boolean} 验证结果 + */ + minLength(value, length) { + if (!value) return true + return value.length >= length + }, + /** + * 最大长度验证 + * @param {string} value 值 + * @param {number} length 最大长度 + * @returns {boolean} 验证结果 + */ + maxLength(value, length) { + if (!value) return true + return value.length <= length + }, + /** + * 数值范围验证 + * @param {number} value 值 + * @param {number} min 最小值 + * @param {number} max 最大值 + * @returns {boolean} 验证结果 + */ + range(value, min, max) { + if (value === undefined || value === null) return true + return value >= min && value <= max + }, + } + return { + /** + * 验证单个字段 + * @param {*} value 值 + * @param {Object} rule 规则 + * @returns {string|null} 错误信息 + */ + validateField(value, rule) { + // 必填验证 + if (rule.required !== undefined && !rules.required(value, rule.required)) { + return rule.message || '此字段为必填项' + } + // 如果值为空且非必填,跳过其他验证 + if (value === undefined || value === null || value === '') { + return null + } + // 其他验证规则 + if (rule.type && rules[rule.type]) { + if (!rules[rule.type](value, rule.param1, rule.param2)) { + return rule.message || '字段格式不正确' + } + } + return null + }, + /** + * 验证整个表单 + * @param {Object} data 表单数据 + * @param {Object} rules 验证规则 + * @returns {Object} 验证结果 { isValid: boolean, errors: Object } + */ + validate(data, rules) { + const errors = {} + let isValid = true + for (let field in rules) { + const error = this.validateField(data[field], rules[field]) + if (error) { + errors[field] = error + isValid = false + } + } + return { isValid, errors } + }, + } + } /** * 微信支付 * @param {Object} paymentData 支付参数 @@ -338,6 +524,11 @@ class Tool { }) }) } + /** + * 文件上传 + * @param {string} filePath 文件路径 + * @returns {Promise} 上传结果 + */ upload(filePath) { return new Promise((resolve, reject) => { uni.uploadFile({ @@ -357,7 +548,120 @@ class Tool { }) }) } + /** + * 暗黑模式切换 + * @param {boolean} isDark 是否启用暗黑模式 + */ + toggleDarkMode(isDark) { + try { + if (isDark) { + // 启用暗黑模式 + document.documentElement.setAttribute('data-theme', 'dark') + uni.setStorageSync('theme', 'dark') + } else { + // 启用亮色模式 + document.documentElement.setAttribute('data-theme', 'light') + uni.setStorageSync('theme', 'light') + } + } catch (error) { + console.error('切换主题失败:', error) + } + } + /** + * 获取当前主题模式 + * @returns {string} 当前主题 ('light' | 'dark') + */ + getCurrentTheme() { + try { + // 优先使用用户设置的主题 + const savedTheme = uni.getStorageSync('theme') + if (savedTheme) { + return savedTheme + } + // 检查系统主题 + const systemInfo = uni.getSystemInfoSync() + if (systemInfo.theme) { + return systemInfo.theme + } + // 默认返回亮色主题 + return 'light' + } catch (error) { + console.error('获取主题失败:', error) + return 'light' + } + } + /** + * 权限检查 + * @param {string} permission 权限标识 + * @returns {boolean} 是否拥有权限 + */ + hasPermission(permission) { + try { + // 获取用户权限列表 + const permissions = uni.getStorageSync('permissions') || [] + return permissions.includes(permission) + } catch (error) { + console.error('权限检查失败:', error) + return false + } + } + /** + * 设置用户权限 + * @param {Array} permissions 权限列表 + */ + setPermissions(permissions) { + try { + uni.setStorageSync('permissions', Array.isArray(permissions) ? permissions : []) + } catch (error) { + console.error('设置权限失败:', error) + } + } + /** + * 获取用户所有权限 + * @returns {Array} 权限列表 + */ + getUserPermissions() { + try { + return uni.getStorageSync('permissions') || [] + } catch (error) { + console.error('获取权限失败:', error) + return [] + } + } + /** + * 检查用户是否有任意权限 + * @param {Array} permissions 权限列表 + * @returns {boolean} 是否有任意权限 + */ + hasAnyPermission(permissions) { + if (!Array.isArray(permissions)) { + return false + } + const userPermissions = this.getUserPermissions() + return permissions.some(permission => userPermissions.includes(permission)) + } + /** + * 检查用户是否拥有所有权限 + * @param {Array} permissions 权限列表 + * @returns {boolean} 是否拥有所有权限 + */ + hasAllPermissions(permissions) { + if (!Array.isArray(permissions)) { + return false + } + const userPermissions = this.getUserPermissions() + return permissions.every(permission => userPermissions.includes(permission)) + } + /** + * 清除用户权限 + */ + clearPermissions() { + try { + uni.removeStorageSync('permissions') + } catch (error) { + console.error('清除权限失败:', error) + } + } } - // 创建单例并导出 export default new Tool() diff --git a/components/common/Card.vue b/components/common/Card.vue new file mode 100644 index 0000000..9c1f744 --- /dev/null +++ b/components/common/Card.vue @@ -0,0 +1,74 @@ + + + + + \ No newline at end of file diff --git a/components/common/CustomButton.vue b/components/common/CustomButton.vue new file mode 100644 index 0000000..eda569f --- /dev/null +++ b/components/common/CustomButton.vue @@ -0,0 +1,195 @@ + + + + + \ No newline at end of file diff --git a/components/common/FormInput.vue b/components/common/FormInput.vue new file mode 100644 index 0000000..ab7b5d2 --- /dev/null +++ b/components/common/FormInput.vue @@ -0,0 +1,141 @@ + + + + + \ No newline at end of file diff --git a/directives/index.js b/directives/index.js new file mode 100644 index 0000000..e8df0a3 --- /dev/null +++ b/directives/index.js @@ -0,0 +1,141 @@ +/** + * 自定义指令入口文件 + */ + +// 防止重复点击指令 +export const preventReClick = { + mounted(el, binding) { + el.addEventListener('click', () => { + if (el.disabled) return + el.disabled = true + setTimeout(() => { + el.disabled = false + }, binding.value || 1000) + }) + }, +} + +// 长按指令 +export const longPress = { + mounted(el, binding) { + let timer = null + let startTime = 0 + const handler = binding.value + const duration = binding.arg || 1000 + + // 取消定时器 + const cancel = () => { + if (timer) { + clearTimeout(timer) + timer = null + } + } + + // 开始计时 + const start = () => { + startTime = Date.now() + timer = setTimeout(() => { + handler(el) + }, duration) + } + + // 绑定事件 + el.addEventListener('touchstart', start) + el.addEventListener('touchend', cancel) + el.addEventListener('touchcancel', cancel) + }, +} + +// 权限控制指令 +export const permission = { + mounted(el, binding) { + const { value } = binding + const permissions = uni.getStorageSync('permissions') || [] + + if (value && !permissions.includes(value)) { + // 隐藏元素 + el.style.display = 'none' + el.__vue__ && (el.__vue__.isDisplay = false) + } + }, + updated(el, binding) { + const { value } = binding + const permissions = uni.getStorageSync('permissions') || [] + + if (value && !permissions.includes(value)) { + // 隐藏元素 + el.style.display = 'none' + el.__vue__ && (el.__vue__.isDisplay = false) + } else { + // 显示元素 + el.style.display = '' + el.__vue__ && (el.__vue__.isDisplay = true) + } + }, +} + +// 拖拽指令 +export const drag = { + mounted(el) { + let startX = 0 + let startY = 0 + let initialX = 0 + let initialY = 0 + let isDragging = false + + // 获取元素初始位置 + const getInitialPosition = () => { + const rect = el.getBoundingClientRect() + initialX = rect.left + initialY = rect.top + } + + // 鼠标按下事件 + const handleMouseDown = e => { + isDragging = true + startX = e.clientX + startY = e.clientY + getInitialPosition() + el.style.position = 'absolute' + el.style.left = initialX + 'px' + el.style.top = initialY + 'px' + el.style.zIndex = 9999 + } + + // 鼠标移动事件 + const handleMouseMove = e => { + if (!isDragging) return + const dx = e.clientX - startX + const dy = e.clientY - startY + el.style.left = initialX + dx + 'px' + el.style.top = initialY + dy + 'px' + } + + // 鼠标松开事件 + const handleMouseUp = () => { + isDragging = false + } + + // 绑定事件 + el.addEventListener('mousedown', handleMouseDown) + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + + // 在元素上存储事件处理函数,以便在指令解绑时移除 + el._dragHandlers = { + handleMouseDown, + handleMouseMove, + handleMouseUp, + } + }, + unmounted(el) { + // 移除事件监听器 + if (el._dragHandlers) { + const { handleMouseDown, handleMouseMove, handleMouseUp } = el._dragHandlers + el.removeEventListener('mousedown', handleMouseDown) + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + delete el._dragHandlers + } + }, +} diff --git a/hooks/index.js b/hooks/index.js new file mode 100644 index 0000000..ee4d229 --- /dev/null +++ b/hooks/index.js @@ -0,0 +1,7 @@ +/** + * Hooks统一导出入口 + */ + +export { useRequest } from './useRequest.js' +export { useState, useStorageState, useFormState } from './useState.js' +export { useGet, usePost } from './useApi.js' \ No newline at end of file diff --git a/hooks/useApi.js b/hooks/useApi.js new file mode 100644 index 0000000..5815892 --- /dev/null +++ b/hooks/useApi.js @@ -0,0 +1,157 @@ +import { ref, reactive } from 'vue' +import { http } from '@/api/index.js' +import tool from '@/common/utils/tool.js' + +/** + * GET请求Hook + * @param {string} url 请求URL + * @param {Object} options 配置选项 + * @returns {Object} 请求相关状态和方法 + */ +export function useGet(url, options = {}) { + const { + manual = false, // 是否手动触发 + params = {}, // 请求参数 + cache = false, // 是否使用缓存 + loadingDelay = 0, // loading延迟时间 + onSuccess = () => {}, // 成功回调 + onError = () => {}, // 失败回调 + } = options + + // 状态 + const data = ref(null) + const loading = ref(false) + const error = ref(null) + + // loading延迟定时器 + let loadingTimer = null + + /** + * 执行GET请求 + * @param {Object} requestParams 请求参数 + */ + async function run(requestParams = {}) { + // 清除之前的loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + + // 如果有loading延迟,先启动定时器 + if (loadingDelay > 0) { + loadingTimer = setTimeout(() => { + loading.value = true + }, loadingDelay) + } else { + loading.value = true + } + + try { + const response = await http.get(url, { + params: { ...params, ...requestParams }, + cache, + }) + // 清除loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + data.value = response.data + error.value = null + onSuccess(response.data) + return response.data + } catch (err) { + // 清除loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + error.value = err + data.value = null + console.error(`GET请求失败: ${url}`, err) + onError(err) + throw err + } finally { + loading.value = false + } + } + + /** + * 取消请求 + */ + function cancel() { + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + loading.value = false + } + + // 如果不是手动触发,立即执行 + if (!manual) { + Promise.resolve().then(() => { + run(params) + }) + } + + return { + data, + loading, + error, + run, + cancel, + } +} + +/** + * POST请求Hook + * @param {string} url 请求URL + * @returns {Object} 请求相关状态和方法 + */ +export function usePost(url) { + /** + * 执行POST请求 + * @param {Object} data 请求数据 + * @param {Object} options 配置选项 + */ + async function run(data, options = {}) { + const { loadingDelay = 0, onSuccess = () => {}, onError = () => {} } = options + + let loadingTimer = null + + // 如果有loading延迟,先启动定时器 + if (loadingDelay > 0) { + loadingTimer = setTimeout(() => { + tool.loading() + }, loadingDelay) + } else { + tool.loading() + } + + try { + const response = await http.post(url, data) + // 清除loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + tool.hideLoading() + onSuccess(response.data) + return response.data + } catch (err) { + // 清除loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + tool.hideLoading() + console.error(`POST请求失败: ${url}`, err) + onError(err) + throw err + } + } + + return { + run, + } +} \ No newline at end of file diff --git a/hooks/useRequest.js b/hooks/useRequest.js new file mode 100644 index 0000000..cec1d35 --- /dev/null +++ b/hooks/useRequest.js @@ -0,0 +1,113 @@ +import { ref, reactive } from 'vue' +import tool from '@/common/utils/tool.js' + +/** + * 通用请求Hook + * @param {Function} apiFunction API函数 + * @param {Object} options 配置选项 + * @returns {Object} 请求相关状态和方法 + */ +export function useRequest(apiFunction, options = {}) { + const { + manual = false, // 是否手动触发 + defaultParams = [], // 默认参数 + onSuccess = () => {}, // 成功回调 + onError = () => {}, // 失败回调 + loadingDelay = 0, // loading延迟时间 + } = options + + // 状态 + const data = ref(null) + const loading = ref(false) + const error = ref(null) + const params = ref([]) + + // loading延迟定时器 + let loadingTimer = null + + /** + * 执行请求 + * @param {...any} args 请求参数 + */ + async function run(...args) { + // 清除之前的loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + + // 设置参数 + params.value = args + + // 如果有loading延迟,先启动定时器 + if (loadingDelay > 0) { + loadingTimer = setTimeout(() => { + loading.value = true + }, loadingDelay) + } else { + loading.value = true + } + + try { + const result = await apiFunction(...args) + // 清除loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + data.value = result + error.value = null + onSuccess(result, args) + return result + } catch (err) { + // 清除loading延迟定时器 + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + error.value = err + data.value = null + console.error('请求失败:', err) + onError(err, args) + throw err + } finally { + loading.value = false + } + } + + /** + * 取消请求 + */ + function cancel() { + if (loadingTimer) { + clearTimeout(loadingTimer) + loadingTimer = null + } + loading.value = false + } + + /** + * 重新执行请求 + */ + function refresh() { + return run(...params.value) + } + + // 如果不是手动触发,立即执行 + if (!manual) { + // 使用nextTick确保在组件挂载后执行 + Promise.resolve().then(() => { + run(...defaultParams) + }) + } + + return { + data, + loading, + error, + params, + run, + cancel, + refresh, + } +} \ No newline at end of file diff --git a/hooks/useState.js b/hooks/useState.js new file mode 100644 index 0000000..d6d46d4 --- /dev/null +++ b/hooks/useState.js @@ -0,0 +1,140 @@ +import { ref, computed } from 'vue' +import tool from '@/common/utils/tool.js' + +/** + * 状态管理Hook + * @param {any} initialState 初始状态 + * @returns {Object} 状态和操作方法 + */ +export function useState(initialState) { + const state = ref(initialState) + + /** + * 设置状态 + * @param {any} newState 新状态 + */ + function setState(newState) { + if (typeof newState === 'function') { + state.value = newState(state.value) + } else { + state.value = newState + } + } + + /** + * 重置状态到初始值 + */ + function resetState() { + state.value = typeof initialState === 'function' ? initialState() : initialState + } + + return { + state: computed(() => state.value), + setState, + resetState, + } +} + +/** + * 本地存储状态Hook + * @param {string} key 存储键名 + * @param {any} defaultValue 默认值 + * @returns {Object} 状态和操作方法 + */ +export function useStorageState(key, defaultValue = null) { + const storageValue = tool.storage(key) ?? defaultValue + const { state, setState, resetState } = useState(storageValue) + + /** + * 设置状态并同步到本地存储 + * @param {any} newState 新状态 + */ + function setStorageState(newState) { + if (typeof newState === 'function') { + newState = newState(state.value) + } + + setState(newState) + tool.storage(key, newState) + } + + /** + * 清除本地存储中的值 + */ + function clearStorage() { + tool.removeStorage(key) + resetState() + } + + return { + state, + setState: setStorageState, + resetState: () => { + clearStorage() + }, + clearStorage, + } +} + +/** + * 表单状态Hook + * @param {Object} initialFormData 初始表单数据 + * @returns {Object} 表单状态和操作方法 + */ +export function useFormState(initialFormData = {}) { + const { state, setState, resetState } = useState({ ...initialFormData }) + + /** + * 设置表单项 + * @param {string} field 字段名 + * @param {any} value 字段值 + */ + function setField(field, value) { + setState(prev => ({ + ...prev, + [field]: value, + })) + } + + /** + * 重置表单到初始状态 + */ + function resetForm() { + resetState() + } + + /** + * 清空表单 + */ + function clearForm() { + setState({}) + } + + /** + * 批量设置表单值 + * @param {Object} values 表单值对象 + */ + function setFields(values) { + setState(prev => ({ + ...prev, + ...values, + })) + } + + /** + * 获取表单验证器 + * @returns {Object} 验证器实例 + */ + function getValidator() { + return tool.getValidator() + } + + return { + form: state, + setField, + resetForm, + clearForm, + setFields, + getValidator, + } +} \ No newline at end of file diff --git a/main.js b/main.js index 9a33a19..e8b5024 100644 --- a/main.js +++ b/main.js @@ -2,18 +2,22 @@ import App from './App' import uviewPlus from '/uview-plus' import globalMixin from './mixins/global' import { createSSRApp } from 'vue' +import { preventReClick, longPress, permission, drag } from './directives/index' import './uni.promisify.adaptor' - uni.$zp = { config: { 'empty-view-text': '空空如也~~', 'refresher-enabled': true, }, } - export function createApp() { const app = createSSRApp(App) app.use(uviewPlus) app.use(globalMixin) + // 注册自定义指令 + app.directive('prevent-re-click', preventReClick) + app.directive('long-press', longPress) + app.directive('permission', permission) + app.directive('drag', drag) return { app } } diff --git a/package.json b/package.json index 3dd694d..0c20c22 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,33 @@ -{ - "dependencies": { - "dotenv": "^17.2.2", - "dayjs": "*", - "vue": "^3.5.21" - } -} +{ + "name": "uniapp-template", + "version": "1.0.0", + "description": "基于 UniApp + Vue3 + uView-Plus 的微信小程序快速开发模板", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint src/**/*.{js,jsx,ts,tsx,vue} --fix", + "format": "prettier --write src/**/*.{js,jsx,ts,tsx,vue,json,css,scss}", + "prepare": "husky install" + }, + "dependencies": { + "dotenv": "^17.2.2", + "dayjs": "*", + "vue": "^3.5.21" + }, + "devDependencies": { + "eslint": "^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "prettier": "^3.0.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "husky": "^8.0.0", + "lint-staged": "^15.0.0" + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,vue}": [ + "eslint --fix", + "prettier --write" + ] + } +} diff --git a/uni.scss b/uni.scss index 98df0ad..ecfa36c 100644 --- a/uni.scss +++ b/uni.scss @@ -21,32 +21,32 @@ $uni-color-warning: #f0ad4e; $uni-color-error: #dd524d; /* 文字基本颜色 */ -$uni-text-color:#333;//基本色 -$uni-text-color-inverse:#fff;//反色 -$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color: #333; //基本色 +$uni-text-color-inverse: #fff; //反色 +$uni-text-color-grey: #999; //辅助灰色,如加载更多的提示信息 $uni-text-color-placeholder: #808080; -$uni-text-color-disable:#c0c0c0; +$uni-text-color-disable: #c0c0c0; /* 背景颜色 */ -$uni-bg-color:#ffffff; -$uni-bg-color-grey:#f8f8f8; -$uni-bg-color-hover:#f1f1f1;//点击状态颜色 -$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 +$uni-bg-color: #ffffff; +$uni-bg-color-grey: #f8f8f8; +$uni-bg-color-hover: #f1f1f1; //点击状态颜色 +$uni-bg-color-mask: rgba(0, 0, 0, 0.4); //遮罩颜色 /* 边框颜色 */ -$uni-border-color:#c8c7cc; +$uni-border-color: #c8c7cc; /* 尺寸变量 */ /* 文字尺寸 */ -$uni-font-size-sm:12px; -$uni-font-size-base:14px; -$uni-font-size-lg:16px; +$uni-font-size-sm: 12px; +$uni-font-size-base: 14px; +$uni-font-size-lg: 16px; /* 图片尺寸 */ -$uni-img-size-sm:20px; -$uni-img-size-base:26px; -$uni-img-size-lg:40px; +$uni-img-size-sm: 20px; +$uni-img-size-base: 26px; +$uni-img-size-lg: 40px; /* Border Radius */ $uni-border-radius-sm: 2px; @@ -68,12 +68,69 @@ $uni-spacing-col-lg: 12px; $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 /* 文章场景相关 */ -$uni-color-title: #2C405A; // 文章标题颜色 -$uni-font-size-title:20px; +$uni-color-title: #2c405a; // 文章标题颜色 +$uni-font-size-title: 20px; $uni-color-subtitle: #555555; // 二级标题颜色 -$uni-font-size-subtitle:26px; -$uni-color-paragraph: #3F536E; // 文章段落颜色 -$uni-font-size-paragraph:15px; +$uni-font-size-subtitle: 26px; +$uni-color-paragraph: #3f536e; // 文章段落颜色 +$uni-font-size-paragraph: 15px; /* 全局资源URL变量 */ -$ASSETSURL:'https://cdn.vrupup.com/s/1732/assets/'; \ No newline at end of file +$ASSETSURL: 'https://cdn.vrupup.com/s/1732/assets/'; + +/* 亮色模式颜色变量 */ +$uni-color-primary-light: #007aff; +$uni-color-success-light: #4cd964; +$uni-color-warning-light: #f0ad4e; +$uni-color-error-light: #dd524d; + +$uni-text-color-light: #333; +$uni-text-color-inverse-light: #fff; +$uni-text-color-grey-light: #999; +$uni-text-color-placeholder-light: #808080; +$uni-text-color-disable-light: #c0c0c0; + +$uni-bg-color-light: #ffffff; +$uni-bg-color-grey-light: #f8f8f8; +$uni-bg-color-hover-light: #f1f1f1; +$uni-bg-color-mask-light: rgba(0, 0, 0, 0.4); + +$uni-border-color-light: #c8c7cc; + +/* 暗色模式颜色变量 */ +$uni-color-primary-dark: #0a84ff; +$uni-color-success-dark: #34c759; +$uni-color-warning-dark: #ff9500; +$uni-color-error-dark: #ff3b30; + +$uni-text-color-dark: #f2f2f7; +$uni-text-color-inverse-dark: #000000; +$uni-text-color-grey-dark: #8e8e93; +$uni-text-color-placeholder-dark: #4c4c4c; +$uni-text-color-disable-dark: #636366; + +$uni-bg-color-dark: #1c1c1e; +$uni-bg-color-grey-dark: #2c2c2e; +$uni-bg-color-hover-dark: #3a3a3c; +$uni-bg-color-mask-dark: rgba(0, 0, 0, 0.6); + +$uni-border-color-dark: #434346; + +/* 默认使用亮色模式颜色 */ +$uni-color-primary: $uni-color-primary-light; +$uni-color-success: $uni-color-success-light; +$uni-color-warning: $uni-color-warning-light; +$uni-color-error: $uni-color-error-light; + +$uni-text-color: $uni-text-color-light; +$uni-text-color-inverse: $uni-text-color-inverse-light; +$uni-text-color-grey: $uni-text-color-grey-light; +$uni-text-color-placeholder: $uni-text-color-placeholder-light; +$uni-text-color-disable: $uni-text-color-disable-light; + +$uni-bg-color: $uni-bg-color-light; +$uni-bg-color-grey: $uni-bg-color-grey-light; +$uni-bg-color-hover: $uni-bg-color-hover-light; +$uni-bg-color-mask: $uni-bg-color-mask-light; + +$uni-border-color: $uni-border-color-light; diff --git a/vite.config.js b/vite.config.js index 3eb7ab5..259a25e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,7 +3,6 @@ import uni from '@dcloudio/vite-plugin-uni' import { resolve } from 'path' import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs' import dotenv from 'dotenv' - // 自定义插件:替换 manifest.json 中的 appid // 仅在通过HBuilder首次编译时执行,避免热重载时重复执行导致内存占用高 function replaceManifestAppid() { @@ -14,18 +13,15 @@ function replaceManifestAppid() { // 检查是否已经执行过插件 const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated') const isFirstCompile = !existsSync(manifestUpdatedFlag) - // 仅首次编译时执行 if (!isFirstCompile) { console.log('跳过 manifest appid 更新(已执行过首次编译)') return } - // 获取环境变量,明确指定路径为项目根目录 dotenv.config({ path: resolve(__dirname, '.env') }) const appid = process.env.VITE_APPID const uni_appId = process.env.VITE_UNI_APPID - if (appid && uni_appId) { // 读取 manifest.json 文件 const manifestPath = resolve(__dirname, 'manifest.json') @@ -34,11 +30,9 @@ function replaceManifestAppid() { const manifest = JSON.parse(manifestContent) // 替换 manifest.appid = uni_appId - if (manifest['mp-weixin']) { manifest['mp-weixin'].appid = appid } - // 写回文件 writeFileSync(manifestPath, JSON.stringify(manifest, null, 4)) // 创建标记文件,表示已执行过插件 @@ -51,21 +45,18 @@ function replaceManifestAppid() { }, } } - // 检查是否通过HBuilder编译,决定是否启用插件 const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated') - if (existsSync(manifestUpdatedFlag)) { unlinkSync(manifestUpdatedFlag) console.log('已清理 manifest 更新标记文件') } - const plugins = manifestUpdatedFlag ? [replaceManifestAppid(), uni()] : [uni()] export default defineConfig({ plugins, resolve: { alias: { - '@': '/src', + '@': resolve(__dirname, 'src'), }, }, build: { @@ -73,6 +64,55 @@ export default defineConfig({ terserOptions: { compress: { drop_console: true, + drop_debugger: true, + pure_funcs: ['console.log'], // 移除console.log + }, + }, + rollupOptions: { + output: { + // 静态资源分类打包 + assetFileNames: (assetInfo) => { + let extType = assetInfo.name.split('.').at(1) + if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) { + extType = 'img' + } else if (/woff|woff2|eot|ttf|otf/i.test(extType)) { + extType = 'font' + } + return `static/${extType}/[name]-[hash].[ext]` + }, + // 分包 + chunkFileNames: 'static/js/[name]-[hash].js', + entryFileNames: 'static/js/[name]-[hash].js', + manualChunks: { + // 将第三方库单独打包 + vue: ['vue'], + 'uview-plus': ['uview-plus'], + 'luch-request': ['luch-request'], + }, + }, + }, + // 启用 CSS 代码分割 + cssCodeSplit: true, + // 小于此阈值的导入或引用资源将内联为 base64 编码 + assetsInlineLimit: 4096, + // 启用 gzip 压缩 + brotliSize: true, + }, + // 优化选项 + optimizeDeps: { + include: ['vue', 'uview-plus', 'luch-request'], + }, + // 开发服务器配置 + server: { + host: '0.0.0.0', + port: 3000, + open: true, + // 代理配置 + proxy: { + '/api': { + target: process.env.VITE_BASE_URL, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), }, }, },