Compare commits

...

22 Commits

Author SHA1 Message Date
yuantao
f8348d80e9 iflow上下文调优 2025-11-05 16:26:01 +08:00
yuantao
65656f1810 新增:全量优化 2025-11-05 16:20:06 +08:00
yuantao
ca6bf7f211 iflow描述文件调优 2025-10-31 16:36:40 +08:00
yuantao
5e1299db5c iflow描述文件调优 2025-10-31 16:30:34 +08:00
yuantao
49c6854e8e 优化原子类样式 2025-10-31 14:51:27 +08:00
yuantao
d5a9bb95c3 iflow提交命令调优 2025-10-31 13:53:08 +08:00
yuantao
c247cbe1c7 iflow描述文件调优 2025-10-29 14:45:00 +08:00
yuantao
7030498a83 iflow描述文件调优 2025-10-29 13:50:28 +08:00
yuantao
edba077718 iflow描述文件调优 2025-10-29 10:02:55 +08:00
yuantao
cee0c70427 导入全局样式变量 2025-10-29 09:58:25 +08:00
yuantao
de533d2ef5 iflow描述文件调优 2025-10-28 18:02:14 +08:00
yuantao
6f9c9e06f1 iflow描述文件调优 2025-10-28 14:44:53 +08:00
yuantao
c7baff94e8 修改iflow描述文件 2025-10-28 13:47:00 +08:00
yuantao
81a16966a4 新增初始化tabbar以及静态资源 2025-10-28 13:00:47 +08:00
yuantao
1f42981fd4 修复:vite编译配置在热重载的情况下导致内存溢出的问题 2025-10-28 11:52:58 +08:00
yuantao
ad0f64a48a 添加初始化目录结构 2025-10-28 10:33:00 +08:00
yuantao
726985d38f 调整iflow配置文件 2025-10-23 14:16:40 +08:00
yuantao
729d5af382 iflow命令修改 2025-10-15 17:33:31 +08:00
yuantao
b5d4684289 Merge branch 'master' of https://git.pandorastudio.cn/yuantao/template-MP 2025-10-15 09:10:41 +08:00
838d5bdb22 Merge branch 'master' of https://git.pandorastudio.cn/yuantao/template-MP 2025-10-15 01:21:25 +08:00
101e3eb2a1 - 添加了三个新的 iFlow CLI 命令配置文件
- cleanproject.toml: 用于清理开发工件的命令
  - commit.toml: 用于分析更改并创建有意义提交消息的命令
  - docs.toml: 用于智能文档管理和更新的命令
  - 这些命令将帮助提高开发效率和项目维护质量
2025-10-15 01:21:22 +08:00
yuantao
2280b5c3f4 优化 项目中可以使用@别名了 2025-10-14 18:00:13 +08:00
36 changed files with 2621 additions and 330 deletions

24
.env
View File

@@ -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
# 接口地址
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

6
.eslintignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.hbuilderx
unpackage
.env*
!.env.example

56
.eslintrc.js Normal file
View File

@@ -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',
},
}

View File

@@ -0,0 +1,11 @@
# Command: cleanproject
# Description: Clean up development artifacts while preserving your working code
# Category: utility
# Version: 1
# Author: 10169
description = "Clean up development artifacts while preserving your working code"
prompt = """
I'll help clean up development artifacts while preserving your working code. I'll identify cleanup targets using native tools: Glob tool to find temporary and debug files, Grep tool to detect debug statements in code, Read tool to verify file contents before removal. Critical directories are automatically protected: .claude directory (commands and configurations), .git directory (version control), node_modules, vendor (dependency directories), Essential configuration files. When I find multiple items to clean, I'll create a todo list to process them systematically.
"""

View File

@@ -0,0 +1,11 @@
# Command: commit
# Description: Analyze your changes and create a meaningful commit message
# Category: utility
# Version: 1
# Author: 10169
description = "分析您的更改并创建有意义的提交消息"
prompt = """
我会分析您的更改并创建一条有意义的提交消息并使用简单的git命令进行提交更改。我会分析更改以确定修改了哪些文件、更改的性质功能、修复、重构等以及受影响的范围/组件。基于分析结果、以及遵循项目的IFLOW上下文中的提交规范我会创建一条常规的中文提交消息其中包含类型新增|修复|文档|优化|其他)、主题(用现在时清晰描述)。
"""

11
.iflow/commands/docs.toml Normal file
View File

@@ -0,0 +1,11 @@
# Command: docs
# Description: Smart documentation management and updates
# Category: documentation
# Version: 1
# Author: 10169
description = "智能文档管理和更新"
prompt = """
我将根据代码变更更新、整理和维护文档,帮助您智能地管理文档。我将同步文档与实现变更,识别过时的章节,提出改进建议,并确保文档之间的一致性。功能包括自动文档更新、交叉引用链接、版本跟踪和质量评估。
"""

View File

@@ -1,16 +1,18 @@
{
"language": "zh-CN",
"mcpServers": {
"Framelink Figma MCP": {
"description": "为AI编程工具提供Figma设计文件访问能力支持获取设计数据和下载图像资源帮助实现设计到代码的一键转换。",
"bing-cn-mcp": {
"description": "MCP 服务器,提供中文必应搜索结果抓取和网页内容抓取功能,支持中文搜索",
"command": "npx",
"args": ["-y", "@iflow-mcp/figma-developer-mcp@0.5.0", "--stdio"],
"args": ["-y", "@iflow-mcp/bing-cn-mcp"],
"env": {
"FIGMA_API_KEY": ""
"USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
},
"playwright": {
"chrome-devtools": {
"description": "Chrome浏览器自动化工具支持页面操作、性能分析、网络监控等功能",
"command": "npx",
"args": ["@playwright/mcp@latest"]
"args": ["-y", "@iflow-mcp/chrome-devtools-mcp"]
},
"context7": {
"description": "为开发者提供最新技术文档和代码示例的智能检索服务,支持库搜索和文档获取功能",
@@ -22,15 +24,6 @@
"command": "npx",
"args": ["-y", "@iflow-mcp/fetch@1.0.2"]
},
"server-puppeteer": {
"description": "基于Puppeteer的浏览器自动化工具支持页面导航、截图、元素操作和JavaScript执行等功能",
"command": "npx",
"args": ["-y", "@iflow-mcp/server-puppeteer@0.6.2"],
"env": {
"PUPPETEER_LAUNCH_OPTIONS": "{\"executablePath\":\"C:\\Program Files\\Google\\Chrome\\Application\"}",
"ALLOW_DANGEROUS": "{\"executablePath\":\"C:\\Program Files\\Google\\Chrome\\Application\"}"
}
},
"math-tools": {
"description": "提供34个数学运算工具包含算术运算、统计计算、数据科学和表达式求值功能支持高精度计算。",
"command": "npx",
@@ -54,10 +47,13 @@
"command": "npx",
"args": ["-y", "@iflow-mcp/server-filesystem@0.6.2", "E:\\"]
},
"mcp-server-code-runner": {
"description": "多语言代码执行器支持35种编程语言的代码片段执行包括JavaScript、Python、Go、Java等主流语言",
"Framelink Figma MCP": {
"description": "为AI编程工具提供Figma设计文件访问能力支持获取设计数据和下载图像资源帮助实现设计到代码的一键转换。",
"command": "npx",
"args": ["-y", "@iflow-mcp/mcp-server-code-runner"]
"args": ["-y", "@iflow-mcp/figma-developer-mcp@0.5.0", "--stdio"],
"env": {
"FIGMA_API_KEY": ""
}
},
"gitlab": {
"description": "提供GitLab项目管理、文件操作、Issue管理、合并请求等功能的MCP服务器工具集",

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.hbuilderx
unpackage
.env*
!.env.example

37
.prettierrc.js Normal file
View File

@@ -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',
}

View File

@@ -9,7 +9,9 @@ export default {
}
</script>
<style lang="scss">
@import "./uni.scss";
@import '@/common/styles/common.css';
@import '@/common/styles/base.scss';
@import '@/common/styles/dark-mode.scss';
@import '@/uview-plus/index.scss';
</style>

128
IFLOW.md
View File

@@ -1,39 +1,52 @@
# 项目概述
# 项目概述 - IFLOW 上下文
这是一个基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数,方便快速开发微信小程序。
这是一个基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数、自定义Hooks、组件等,方便快速开发微信小程序。
## 技术栈
* **UniApp**: 跨平台开发框架,用于构建微信小程序。
* **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面。
* **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面,采用 Composition API 和 setup 语法糖
* **uView-Plus**: 基于 UniApp 的 UI 组件库。
* **z-paging**: 一个用于处理分页加载的组件库。
* **Vuex**: 状态管理库
* **luch-request**: 网络请求库,用于封装 HTTP 请求
* **ESLint & Prettier**: 代码规范和格式化工具。
## 目录结构
```
.
├── api/ # 接口相关
│ ├── modules/ # 业务接口
│ ├── modules/ # 业务接口模块
│ ├── index.js # API统一导出入口
│ └── request.js # 请求封装
├── common/ # 公共资源
│ ├── constants/ # 常量定义
│ ├── styles/ # 全局样式
│ │ ├── common.css # codefun原子类样式
│ │ ── base.scss # 全局样式变量
│ │ ── base.scss # 全局样式变量和工具类
│ │ └── dark-mode.scss # 暗黑模式样式
│ └── utils/ # 工具函数
── tool.ts # 常用工具函数
── tool.js # 常用工具函数
│ └── env.js # 环境变量工具
├── components/ # 公共组件
│ └── common/ # 通用业务组件
├── directives/ # 自定义指令
├── hooks/ # Vue Composition API Hooks
│ ├── index.js # Hooks统一导出入口
│ ├── useRequest.js # 通用请求Hook
│ ├── useApi.js # API相关Hook
│ └── useState.js # 状态管理Hook
├── uni_modules/ # uni-app 组件
│ └── z-paging/ # 分页组件库
├── lib/ # 第三方库
│ └── luch-request/ # luch-request 网络请求库
├── uview-plus/ # uView-Plus 组件库
├── mixins/ # Vue 混入
│ └── global.ts # 全局混入
├── store/ # 状态管理
│ └── index.ts # Vuex store
│ └── global.js # 全局混入
├── pages/ # 主包页面
├── static/ # 静态资源文件
│ └── assets/ # 静态图片资源
├── store/ # 状态管理
├── subPages/ # 分包页面
├── App.vue # 应用入口
├── main.js # 主入口文件
@@ -42,7 +55,11 @@
├── uni.scss # 全局样式变量
├── vite.config.js # Vite 编译配置
├── .nvmdrc # Node.js 版本要求
── .env # 环境变量
── .env # 环境变量
├── .eslintrc.js # ESLint 配置
├── .prettierrc.js # Prettier 配置
├── .eslintignore # ESLint 忽略文件
└── .prettierignore # Prettier 忽略文件
```
# 开发环境与运行
@@ -60,11 +77,30 @@ npm install
## 运行项目
由于这是一个 UniApp 项目,通常需要使用 HBuilderX 或其他支持 UniApp 的 IDE 来运行和调试。具体的运行命令取决于你的开发环境。
```bash
# 开发环境运行
npm run dev
# 构建生产环境
npm run build
# 预览构建结果
npm run preview
# 代码检查和修复
npm run lint
# 代码格式化
npm run format
```
## 构建项目
同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具
构建项目使用 Vite 工具链:
```bash
npm run build
```
# 代码规范与开发约定
@@ -73,11 +109,12 @@ npm install
* 全局样式文件位于 `common/styles/` 目录下,包括 `common.css``base.scss`
* `common.css` 提供了codefun原子类样式用于快速布局。
* `base.scss` 提供了SCSS变量和mixins以及常用的样式类生成器。
* 支持暗黑模式,通过 `dark-mode.scss` 实现。
* 样式规范应遵循项目中已有的风格。
## JavaScript
* 严格遵循ES6规范。
* 严格遵循ES6+规范。
* 遵循JavaScript函数式编程范式。
* 方法类函数应该使用 `function` 进行定义。
* 避免出现超过4个以上的 `ref`超过4个则使用 `reactive`
@@ -89,13 +126,14 @@ npm install
* 事件类方法命名参考 `onClick``onSelect`
* 变量都应该写有注释说明、类型说明。
* 所有 `Promise` 类方法使用 `async` `await` 写法,避免出现 `.then` 嵌套,并进行容错、错误抛出处理。
* 在需要页面跳转、提示、加载、本地存储、或其他功能的时候,优先使用工具函数 `common/utils/tool.js` 中存在的函数。
* 字符串拼接使用ES6的模板语法。
* JavaScript规范应遵循项目中已有的风格
* 组件使用 Vue3 的 Composition API (setup语法糖) 编写
## 静态资源
* 静态资源变量 `ASSETSURL` 已进行全局混入,可以在 `<template></template>` 中直接使用。
* 所有静态资源URL应该使用 `ASSETSURL` 进行拼接,`${ASSETSURL}simple.png`
* 所有静态资源URL应该使用 `ASSETSURL` 进行拼接,使用方式为`${ASSETSURL}simple.png``background-image: url($ASSETSURL + 'b23bbf0c4c8e59f88f8fd883cb5d6b27.png')`
## 工具函数 (tool.js)
@@ -105,26 +143,74 @@ npm install
* **页面跳转**: `navigateTo`, `redirectTo`, `reLaunch`, `switchTab`, `navigateBack`
* **本地存储**: `storage`, `removeStorage`, `getStorageInfo`
* **其他功能**: `copy` (复制文本), `saveImageToPhotos` (保存图片), `requestPayment` (微信支付), `upload` (文件上传), `loadFont` (加载字体)
* **日期处理**: `formatDate` (日期格式化)
* **工具函数**: `deepClone` (深拷贝), `debounce/throttle` (防抖节流)
* **表单验证**: `getValidator` (表单验证工具)
* **主题切换**: `toggleDarkMode`, `getCurrentTheme` (暗黑模式切换)
* **权限管理**: `hasPermission`, `setPermissions`, `getUserPermissions` (权限检查和管理)
## 网络请求
* 网络请求使用 `lib/luch-request` 库进行封装。
* 全局配置在 `api/request.js` 中定义包括基础URL、请求头、SSL验证等。
* 包含请求和响应拦截器,用于处理通用逻辑(如错误提示、鉴权等)。
* 支持请求缓存机制和重试机制。
* 各业务板块的接口都应存放在 `api/modules` 下,并将单个接口进行导出以便页面按需导入。
* 提供 `useGet``usePost` 等 Hooks 用于在组件中便捷地发起请求。
## 自定义Hooks
项目提供了多个可复用的 Vue Composition API Hooks位于 `hooks/` 目录下:
* `useRequest`: 通用请求Hook支持自动执行、手动执行、loading状态等
* `useGet`: GET请求Hook
* `usePost`: POST请求Hook
* `useState`: 状态管理Hook
* `useStorageState`: 本地存储状态Hook
* `useFormState`: 表单状态Hook
## 组件
* 项目集成了 `uView-Plus``z-paging` 两个组件库,组件均已全局导入,可以直接使用
* 项目集成了 `uView-Plus``z-paging` 两个组件库。
* 所有 `uni_modules` 目录中的组件无需导入直接可以进行使用。
* `uView-Plus` 组件已通过 `easycom` 自动导入,可以直接使用,如:`<u-button>``<u-icon>`
* 全局组件放在 `components/` 目录下
* 自定义公共组件放在 `components/common/` 目录下,采用 Vue3 的 setup 语法糖编写
* 页面独立组件放在页面根目录下的 `components/`
* 每个组件应该附带 `README.MD` 文档
* 微信的原生组件放在页面根目录下的 `wxcomponents/`,并在使用了组件的对应页面路由配置中添加组件的引用属性 `"usingComponents": { "components": "/wxcomponents/components/components" }`
* 组件编写应遵循项目中已有的风格。
## 自定义指令
* 项目支持自定义指令,位于 `directives/` 目录下
* 包括防重复点击、长按、权限控制、拖拽等指令
*`main.js` 中已全局注册
## 常量管理
* 项目常量统一管理在 `common/constants/` 目录下
* 包括应用信息、页面路径、存储键名、事件常量等
## 环境变量
* 环境变量通过 `.env` 文件配置
* 提供 `common/utils/env.js` 工具进行环境变量验证和获取
## 页面
* 页面配置在 `pages.json` 中管理。
* 主包页面放在 `pages/` 目录下,分包页面放在 `subPages/` 目录下。
* 主包页面放在 `pages/` 目录下,分包页面放在 `subPages/` 目录下,如果页面不属于一级页面且没有包含在 `pages.json` 中的 `tabbar`,则应该放置在分包目录下
* 页面使用 Composition API (setup语法糖) 编写。
* 注释、结构规范应遵循项目中已有的风格。
* 注释、结构规范应遵循项目中已有的风格。
## 代码提交规范
* 提交信息应清晰描述变更内容,如 `修复 搜索功能空值检查``新增 删除按钮功能`
* 对于功能性的新增或修改,使用 `新增` 前缀。
* 对于错误修复,使用 `修复` 前缀。
* 对于性能优化、代码重构(既不修复错误也不添加功能),使用 `优化` 前缀。
* 对于文档更新,使用 `文档` 前缀。
* 提交信息应使用中文。
## 其他
* 页面中的分享功能应该使用原生的微信分享功能,通过 `button``<u-button>` 组件的 `open-type="share"` 属性实现。

239
README.md
View File

@@ -1,80 +1,159 @@
# <EFBFBD><EFBFBD>Ŀģ<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>˵<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģ<EFBFBD><EFBFBD><EFBFBD>ǻ<EFBFBD><EFBFBD><EFBFBD> UniApp + Vue3 + uView-Plus <EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀģ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һЩ<EFBFBD><EFBFBD><EFBFBD>õ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>á<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͹<EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
## Ŀ¼<EFBFBD>
```
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> api/ # <EFBFBD>ӿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> modules/ # ҵ<EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> request.js # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> common/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> styles/ # ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> common.css # codefunԭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> base.scss # ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> utils/ # <EFBFBD><EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> tool.ts # <EFBFBD><EFBFBD><EFBFBD>ù<EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> components/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uni_modules/ # uni-app <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> z-paging/ # <EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> lib/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> luch-request/ # luch-request <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uview-plus/ # uView-Plus <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> mixins/ # Vue <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> global.ts # ȫ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> store/ # ״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> index.ts # Vuex store
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages/ # <20><><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> subPages/ # <EFBFBD>ְ<EFBFBD>ҳ<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> App.vue # Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> main.js # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages.json # ҳ<><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> manifest.json # Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uni.scss # ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> vite.config.js # Vite <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .nvmdrc # Node.js <20>汾Ҫ<E6B1BE><D2AA>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .env # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
```
## ʹ<>÷<EFBFBD><C3B7><EFBFBD>
1. <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģ<EFBFBD><EFBFBD>Ŀ¼<EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ĿĿ¼<EFBFBD><EFBFBD>
2. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>޸<EFBFBD> package.json <20>е<EFBFBD><D0B5><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD>ƺ<EFBFBD><C6BA><EFBFBD><EFBFBD><EFBFBD>
3. ʹ<EFBFBD><EFBFBD> npm install <20><>װ<EFBFBD><D7B0><EFBFBD><EFBFBD>
4. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>޸<EFBFBD> pages.json <20>е<EFBFBD>ҳ<EFBFBD><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
5. <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¹<EFBFBD><C2B9><EFBFBD>
## <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD>
### <20><>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- **dotenv** - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>
### <20><>ʽ
- common.css: ȫ<>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD><EFBFBD>ʽ
- base.scss: <20><><EFBFBD>õ<EFBFBD> SCSS <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
### <20><><EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD> (tool.js)
- alert: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ
- loading/hideLoading: <20><>ʾ/<2F><><EFBFBD>ؼ<EFBFBD><D8BC><EFBFBD><EFBFBD><EFBFBD>ʾ
- ҳ<><D2B3><EFBFBD><EFBFBD>ת<EFBFBD><D7AA><EFBFBD>ط<EFBFBD><D8B7><EFBFBD>: navigateTo, redirectTo, reLaunch, switchTab, navigateBack
- <20><><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: storage, removeStorage, getStorageInfo
- copy: <20><><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- saveImageToPhotos: <20><><EFBFBD><EFBFBD>ͼƬ<CDBC><C6AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- requestPayment: ΢<><CEA2>֧<EFBFBD><D6A7>
- upload: <20>ļ<EFBFBD><C4BC>ϴ<EFBFBD>
### <20><><EFBFBD><EFBFBD>
- App.vue: ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- main.js: Vue Ӧ<>ó<EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>ȫ<EFBFBD>ֲ<EFBFBD><D6B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- pages.json: ҳ<><D2B3>·<EFBFBD>ɺʹ<C9BA><CDB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- uni.scss: ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD>
## ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
1. <20><><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD>޸Ļ<DEB8><C4BB><EFBFBD>չ<EFBFBD><D5B9><EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD>
3. <20><><EFBFBD><EFBFBD><EFBFBD>ɸ<EFBFBD><C9B8><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD>޸Ļ<DEB8><C4BB>
4. <20><>ʽ<EFBFBD>ļ<EFBFBD><C4BC>ɸ<EFBFBD><C9B8><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD>ƹ淶<C6B9><E6B7B6><EFBFBD>е<EFBFBD><D0B5><EFBFBD>
# 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` 已进行全局混入,可以在 `<template></template>` 中直接使用
### 网络请求
- 网络请求使用 `lib/luch-request` 库进行封装
- 包含请求和响应拦截器,用于处理通用逻辑
- 各业务板块的接口都应存放在 `api/modules`
### 组件
- 所有 `uni_modules` 目录中的组件无需导入直接可以进行使用
- `uView-Plus` 组件已通过 `easycom` 自动导入
### 页面
- 页面配置在 `pages.json` 中管理
- 页面使用 Composition API (setup语法糖) 编写
## 代码提交规范
- 提交信息应清晰描述变更内容
- 对于功能性的新增或修改,使用 `新增` 前缀
- 对于错误修复,使用 `修复` 前缀
- 对于性能优化、代码重构,使用 `优化` 前缀
- 对于文档更新,使用 `文档` 前缀
- 提交信息应使用中文
## 注意事项
1. 请根据实际项目需求进行修改和扩展功能
2. 代码文件可按项目规范进行修改替换
3. 样式文件可按项目规范进行修改

15
api/index.js Normal file
View File

@@ -0,0 +1,15 @@
/**
* API统一导出入口
*/
// 导入网络请求实例
import http from './request.js'
// 导入各业务模块
import user from './modules/user.js'
// 导出网络请求实例
export { http }
// 导出各业务模块
export { user }
// 默认导出包含所有模块的对象
export default {
user,
}

85
api/modules/user.js Normal file
View File

@@ -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}`)
},
}

View File

@@ -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)
@@ -11,31 +48,115 @@ http.setConfig(config => {
}
/* 设置全局配置 */
http.setConfig(config => {
config.header = { ...config.header }
config.sslVerify = false
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
http.interceptors.response.use(response => {
if (response.statusCode == 500 || response.statusCode == 404 || response.statusCode == 403) {
console.error(response)
return tool.alert('网络错误,请稍后重试')
},
config => {
return Promise.reject(config)
}
)
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('网络连接失败,请检查网络')
if (response.statusCode == 401 || response.data.code == 401) {
}
if (response.statusCode == 200) {
return Promise.resolve(response)
} else {
return Promise.reject(response)
}
})
} else {
tool.alert('请求失败,请稍后重试')
}
return Promise.reject(error)
}
)
// 添加缓存功能到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)
}
}

30
common/constants/app.js Normal file
View File

@@ -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'
}

View File

@@ -1,118 +1,97 @@
// 定义内外边距,历遍1-40
@for $i from 0 through 40 {
// 只要双数和能被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;
}

View File

@@ -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);
}

80
common/utils/env.js Normal file
View File

@@ -0,0 +1,80 @@
/**
* 环境变量验证工具
*/
/**
* 验证必需的环境变量
* @param {Array<string>} 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

View File

@@ -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<Object>} 上传结果
*/
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<string>} permissions 权限列表
*/
setPermissions(permissions) {
try {
uni.setStorageSync('permissions', Array.isArray(permissions) ? permissions : [])
} catch (error) {
console.error('设置权限失败:', error)
}
}
/**
* 获取用户所有权限
* @returns {Array<string>} 权限列表
*/
getUserPermissions() {
try {
return uni.getStorageSync('permissions') || []
} catch (error) {
console.error('获取权限失败:', error)
return []
}
}
/**
* 检查用户是否有任意权限
* @param {Array<string>} permissions 权限列表
* @returns {boolean} 是否有任意权限
*/
hasAnyPermission(permissions) {
if (!Array.isArray(permissions)) {
return false
}
const userPermissions = this.getUserPermissions()
return permissions.some(permission => userPermissions.includes(permission))
}
/**
* 检查用户是否拥有所有权限
* @param {Array<string>} 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()

View File

@@ -0,0 +1,74 @@
<template>
<view class="card" :class="{ 'card--shadow': shadow, 'card--border': border }">
<view class="card__header" v-if="title || $slots.header">
<slot name="header">
<view class="card__title">{{ title }}</view>
</slot>
</view>
<view class="card__body">
<slot></slot>
</view>
<view class="card__footer" v-if="$slots.footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script setup>
// 定义props
defineProps({
// 卡片标题
title: {
type: String,
default: ''
},
// 是否显示阴影
shadow: {
type: Boolean,
default: false
},
// 是否显示边框
border: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.card {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
// 阴影样式
&--shadow {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
// 边框样式
&--border {
border: 1rpx solid #f0f0f0;
}
&__header {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
&__body {
padding: 32rpx;
}
&__footer {
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<view
class="custom-button"
:class="[`custom-button--${type}`, `custom-button--${size}`, { 'custom-button--disabled': disabled, 'custom-button--loading': loading }]"
@click="handleClick"
>
<view class="custom-button__loading" v-if="loading">
<u-loading-icon mode="circle" size="20" color="#fff" />
</view>
<view class="custom-button__content">
<slot></slot>
</view>
</view>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue'
// 定义props
const props = defineProps({
// 按钮类型
type: {
type: String,
default: 'primary', // primary, secondary, danger, ghost
validator: (value) => ['primary', 'secondary', 'danger', 'ghost'].includes(value)
},
// 按钮大小
size: {
type: String,
default: 'medium', // small, medium, large
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 防抖时间(毫秒)
debounce: {
type: Number,
default: 0
}
})
// 定义emits
const emit = defineEmits(['click'])
// 防抖定时器
const debounceTimer = ref(null)
/**
* 处理点击事件
* @param {Event} e 点击事件对象
*/
function handleClick(e) {
// 如果禁用或加载中,不处理点击
if (props.disabled || props.loading) {
return
}
// 如果设置了防抖
if (props.debounce > 0) {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
}
debounceTimer.value = setTimeout(() => {
emit('click', e)
debounceTimer.value = null
}, props.debounce)
} else {
emit('click', e)
}
}
// 组件卸载前清除定时器
onBeforeUnmount(() => {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
debounceTimer.value = null
}
})
</script>
<style lang="scss" scoped>
.custom-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-weight: 500;
text-align: center;
vertical-align: middle;
cursor: pointer;
transition: all 0.2s;
user-select: none;
border: 1rpx solid transparent;
position: relative;
overflow: hidden;
// 主要按钮
&--primary {
color: #fff;
background-color: #1890ff;
border-color: #1890ff;
&:not(.custom-button--disabled):active {
background-color: #40a9ff;
border-color: #40a9ff;
}
}
// 次要按钮
&--secondary {
color: #333;
background-color: #f5f5f5;
border-color: #d9d9d9;
&:not(.custom-button--disabled):active {
background-color: #e6e6e6;
border-color: #bfbfbf;
}
}
// 危险按钮
&--danger {
color: #fff;
background-color: #ff4d4f;
border-color: #ff4d4f;
&:not(.custom-button--disabled):active {
background-color: #ff7875;
border-color: #ff7875;
}
}
// 幽灵按钮
&--ghost {
color: #1890ff;
background-color: transparent;
border-color: #1890ff;
&:not(.custom-button--disabled):active {
color: #40a9ff;
border-color: #40a9ff;
}
}
// 禁用状态
&--disabled {
cursor: not-allowed;
opacity: 0.6;
}
// 加载状态
&--loading {
cursor: not-allowed;
}
// 小尺寸
&--small {
height: 56rpx;
padding: 0 24rpx;
font-size: 24rpx;
}
// 中等尺寸
&--medium {
height: 72rpx;
padding: 0 32rpx;
font-size: 28rpx;
}
// 大尺寸
&--large {
height: 88rpx;
padding: 0 40rpx;
font-size: 32rpx;
}
&__loading {
margin-right: 16rpx;
}
&__content {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<view class="form-input">
<view class="form-input__label" v-if="label">
{{ label }}
<text class="form-input__required" v-if="required">*</text>
</view>
<view class="form-input__wrapper">
<input
class="form-input__field"
:type="type"
:placeholder="placeholder"
:value="modelValue"
:disabled="disabled"
:maxlength="maxlength"
@input="handleInput"
@blur="handleBlur"
@focus="handleFocus"
/>
<view class="form-input__suffix" v-if="$slots.suffix">
<slot name="suffix"></slot>
</view>
</view>
<view class="form-input__error" v-if="error">{{ error }}</view>
</view>
</template>
<script setup>
// 定义props
const props = defineProps({
// 输入框的值
modelValue: {
type: [String, Number],
default: ''
},
// 标签
label: {
type: String,
default: ''
},
// 占位符
placeholder: {
type: String,
default: ''
},
// 输入框类型
type: {
type: String,
default: 'text'
},
// 是否必填
required: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 最大长度
maxlength: {
type: Number,
default: 140
},
// 错误信息
error: {
type: String,
default: ''
}
})
// 定义emits
const emit = defineEmits(['update:modelValue', 'blur', 'focus'])
/**
* 处理输入事件
* @param {Event} e 输入事件对象
*/
function handleInput(e) {
emit('update:modelValue', e.detail.value)
}
/**
* 处理失去焦点事件
* @param {Event} e 失去焦点事件对象
*/
function handleBlur(e) {
emit('blur', e.detail.value)
}
/**
* 处理获得焦点事件
* @param {Event} e 获得焦点事件对象
*/
function handleFocus(e) {
emit('focus', e.detail.value)
}
</script>
<style lang="scss" scoped>
.form-input {
margin-bottom: 32rpx;
&__label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
&__required {
color: #ff4d4f;
margin-left: 4rpx;
}
&__wrapper {
display: flex;
align-items: center;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
padding: 0 24rpx;
background-color: #fff;
}
&__field {
flex: 1;
height: 80rpx;
font-size: 28rpx;
color: #333;
}
&__suffix {
margin-left: 16rpx;
}
&__error {
font-size: 24rpx;
color: #ff4d4f;
margin-top: 8rpx;
}
}
</style>

141
directives/index.js Normal file
View File

@@ -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
}
},
}

7
hooks/index.js Normal file
View File

@@ -0,0 +1,7 @@
/**
* Hooks统一导出入口
*/
export { useRequest } from './useRequest.js'
export { useState, useStorageState, useFormState } from './useState.js'
export { useGet, usePost } from './useApi.js'

157
hooks/useApi.js Normal file
View File

@@ -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,
}
}

113
hooks/useRequest.js Normal file
View File

@@ -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,
}
}

140
hooks/useState.js Normal file
View File

@@ -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,
}
}

View File

@@ -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 }
}

View File

@@ -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"
]
}
}

View File

@@ -7,9 +7,27 @@
"subPackages": [
{
"root": "subPages",
"pages": []
"pages": [
{
"path": "welcome/index"
}
]
}
],
"tabBar": {
"color": "#c3c3c3",
"selectedColor": "#454c5c",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/assets/icon_home.png",
"selectedIconPath": "static/assets/icon_home_selected.png"
}
]
},
"easycom": {
"autoscan": true,
"custom": {

BIN
static/assets/icon_home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

100
uni.scss
View File

@@ -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,9 +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/';
/* 亮色模式颜色变量 */
$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;

View File

@@ -1,36 +1,42 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { resolve } from 'path'
import { readFileSync, writeFileSync } from 'fs'
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import dotenv from 'dotenv'
// 自定义插件:替换 manifest.json 中的 appid
// 仅在通过HBuilder首次编译时执行避免热重载时重复执行导致内存占用高
function replaceManifestAppid() {
return {
name: 'replace-manifest-appid',
buildStart() {
// 检查是否通过HBuilder编译
// 检查是否已经执行过插件
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')
let manifestContent = readFileSync(manifestPath, 'utf-8')
// 解析 JSON
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))
// 创建标记文件,表示已执行过插件
writeFileSync(manifestUpdatedFlag, 'Manifest updated by HBuilder first compile')
console.log(`Manifest appid 已更新为: ${uni_appId}`)
console.log(`Manifest mp-weixin appid 已更新为: ${appid}`)
} else {
@@ -39,14 +45,74 @@ 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: [replaceManifestAppid(), uni()],
plugins,
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
minify: 'terser',
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/, ''),
},
},
},