This commit is contained in:
2025-06-25 12:27:52 +08:00
commit 960c4b7da4
190 changed files with 38050 additions and 0 deletions

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
# 表示所有文件适用
[*]
charset = utf-8 # 设置文件字符集为 utf-8
end_of_line = lf # 控制换行类型(lf | cr | crlf)
indent_style = space # 缩进风格tab | space
indent_size = 2 # 缩进大小
insert_final_newline = true # 始终在文件末尾插入一个新行
# 表示仅 md 文件适用以下规则
[*.md]
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

10
.env.development Normal file
View File

@ -0,0 +1,10 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
# 项目运行的端口号
VITE_APP_PORT = 4096
# API 基础路径,开发环境下的请求前缀
VITE_APP_BASE_API = '/dev-api'
# API 服务器的 URL
VITE_APP_API_URL = http://localhost:8989

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
# API 基础路径,开发环境下的请求前缀
VITE_APP_BASE_API = '/prod-api'

View File

@ -0,0 +1,98 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onAddToFavorites": true,
"onBackPress": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onError": true,
"onErrorCaptured": true,
"onHide": true,
"onLaunch": true,
"onLoad": true,
"onMounted": true,
"onNavigationBarButtonTap": true,
"onNavigationBarSearchInputChanged": true,
"onNavigationBarSearchInputClicked": true,
"onNavigationBarSearchInputConfirmed": true,
"onNavigationBarSearchInputFocusChanged": true,
"onPageNotFound": true,
"onPageScroll": true,
"onPullDownRefresh": true,
"onReachBottom": true,
"onReady": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onResize": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onShareAppMessage": true,
"onShareTimeline": true,
"onShow": true,
"onTabItemTap": true,
"onThemeChange": true,
"onUnhandledRejection": true,
"onUnload": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useModel": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"DirectiveBinding": true,
"MaybeRef": true,
"MaybeRefOrGetter": true
}
}

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
.history
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
launch.json
.hbuilderx
pnpm-lock.yaml

1
.husky/commit-msg Normal file
View File

@ -0,0 +1 @@
npx --no-install commitlint --edit $1

2
.husky/pre-commit Normal file
View File

@ -0,0 +1,2 @@
echo "Running pre-commit hook..."
pnpm run lint:lint-staged

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
*.min.js
dist
public
node_modules
auto-imports.d.ts

41
.prettierrc.yaml Normal file
View File

@ -0,0 +1,41 @@
# 在单参数箭头函数中始终添加括号
arrowParens: "always"
# JSX 多行元素的闭合标签另起一行
bracketSameLine: false
# 对象字面量中的括号之间添加空格
bracketSpacing: true
# 自动格式化嵌入的代码(如 Markdown 和 HTML 内的代码)
embeddedLanguageFormatting: "auto"
# 忽略 HTML 空白敏感度,将空白视为非重要内容
htmlWhitespaceSensitivity: "ignore"
# 不插入 @prettier 的 pragma 注释
insertPragma: false
# 在 JSX 中使用双引号
jsxSingleQuote: false
# 每行代码的最大长度限制为 100 字符
printWidth: 100
# 在 Markdown 中保留原有的换行格式
proseWrap: "preserve"
# 仅在必要时添加对象属性的引号
quoteProps: "as-needed"
# 不要求文件开头插入 @prettier 的 pragma 注释
requirePragma: false
# 在语句末尾添加分号
semi: true
# 使用双引号而不是单引号
singleQuote: false
# 缩进使用 2 个空格
tabWidth: 2
# 在多行元素的末尾添加逗号ES5 支持的对象、数组等)
trailingComma: "es5"
# 使用空格而不是制表符缩进
useTabs: false
# Vue 文件中的 <script> 和 <style> 不增加额外的缩进
vueIndentScriptAndStyle: false
# 根据系统自动检测换行符
endOfLine: "auto"
# 对 HTML 文件应用特定格式化规则
overrides:
- files: "*.html"
options:
parser: "html"

5
.stylelintignore Normal file
View File

@ -0,0 +1,5 @@
*.min.js
dist
public
node_modules

55
.stylelintrc.json Normal file
View File

@ -0,0 +1,55 @@
{
"extends": [
"stylelint-config-recommended",
"stylelint-config-recommended-scss",
"stylelint-config-recommended-vue/scss",
"stylelint-config-html/vue",
"stylelint-config-recess-order"
],
"plugins": ["stylelint-prettier"],
"overrides": [
{
"files": ["**/*.{vue,html}"],
"customSyntax": "postcss-html"
},
{
"files": ["**/*.{css,scss}"],
"customSyntax": "postcss-scss"
}
],
"rules": {
"import-notation": "string",
"selector-class-pattern": null,
"custom-property-pattern": null,
"keyframes-name-pattern": null,
"no-descending-specificity": null,
"no-empty-source": null,
"declaration-property-value-no-unknown": null,
"unit-no-unknown": null,
"selector-type-no-unknown": [
true,
{
"ignoreTypes": ["page"]
}
],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "export", "deep"]
}
],
"property-no-unknown": [
true,
{
"ignoreProperties": []
}
],
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": ["apply", "use", "forward"]
}
]
}
}

12
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"stylelint.validate": ["css", "scss", "vue", "html"]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-present 郝先瑞
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

926
README.md Normal file
View File

@ -0,0 +1,926 @@
# 中医慢病管理平台项目文档
## 📋 项目概述
### 项目简介
本项目是一个基于 uni-app + Vue 3 + TypeScript 的移动端跨平台健康管理应用,旨在为用户提供全方位的数字化健康管理服务。平台集成了传统中医理论与现代医学技术,为用户提供个性化的健康管理解决方案。
### 核心价值
- **一站式健康管理**:集成多种健康功能,提供完整的健康管理闭环
- **智能化数据分析**:基于用户健康数据,提供个性化建议和预警
- **中西医结合**:融合传统中医理论与现代医学检测技术
- **专业医疗服务**:提供在线问诊、专家咨询等专业医疗服务
- **持续健康监测**:长期跟踪用户健康状态,形成健康档案
### 目标用户
- **慢病患者**:糖尿病、高血压等慢性疾病患者
- **健康管理爱好者**:注重健康生活方式的用户
- **中老年群体**:需要定期健康监测的用户
- **亚健康人群**:工作压力大、生活不规律的都市人群
- **医疗专业人士**:需要远程患者管理的医护人员
## 🛠 技术架构
### 技术栈
#### 前端技术
- **框架**: uni-app支持多端发布
- **UI框架**: Vue 3 Composition API
- **类型检查**: TypeScript
- **状态管理**: Pinia
- **样式处理**: SCSS
- **图表组件**: qiun-data-charts
- **工具库**: lodash, dayjs
#### 开发工具
- **开发环境**: HBuilderX / VS Code
- **版本控制**: Git
- **包管理**: npm / yarn
- **代码规范**: ESLint + Prettier
- **API管理**: 基于Promise的请求封装
#### 部署平台
- **APP**: Android、iOS
- **小程序**: 微信、支付宝、百度、字节跳动
- **H5**: 移动端浏览器
### 系统架构
```
┌─────────────────────────────────────┐
│ 展示层 (Presentation) │
├─────────────────────────────────────┤
│ Vue 3 + TypeScript + uni-app │
│ ├─ 健康管理首页 │
│ ├─ 在线问诊模块 │
│ ├─ 健康检测模块 │
│ ├─ 慢病管理模块 │
│ ├─ 膳食管理模块 │
│ ├─ 体质辩识模块 │
│ ├─ 运动管理模块 │
│ ├─ 中医百科模块 │
│ ├─ 健康讲堂模块 │
│ └─ 个人档案模块 │
├─────────────────────────────────────┤
│ 业务层 (Business) │
├─────────────────────────────────────┤
│ 状态管理 (Pinia Store) │
│ ├─ 用户状态管理 │
│ ├─ 健康数据管理 │
│ ├─ 咨询会话管理 │
│ └─ 缓存数据管理 │
├─────────────────────────────────────┤
│ 数据层 (Data) │
├─────────────────────────────────────┤
│ API接口服务 │
│ ├─ RESTful API │
│ ├─ WebSocket (实时通信) │
│ ├─ 文件上传服务 │
│ └─ 数据同步服务 │
├─────────────────────────────────────┤
│ 工具层 (Utils) │
├─────────────────────────────────────┤
│ ├─ 健康计算工具 │
│ ├─ 数据验证工具 │
│ ├─ 格式化工具 │
│ ├─ 提醒服务 │
│ └─ 缓存管理 │
└─────────────────────────────────────┘
```
## 📁 项目结构
```
src/
├─ api/ # API接口
│ ├─ auth/ # 认证相关API
│ ├─ file/ # 文件上传API
│ ├─ system/ # 系统API
│ └─ health/ # 健康管理API
│ ├─ consultation.ts # 在线问诊API
│ ├─ detection.ts # 健康检测API
│ ├─ chronic.ts # 慢病管理API
│ ├─ diet.ts # 膳食管理API
│ ├─ constitution.ts # 体质辩识API
│ ├─ exercise.ts # 运动管理API
│ ├─ encyclopedia.ts # 中医百科API
│ ├─ education.ts # 健康讲堂API
│ └─ profile.ts # 个人档案API
├─ components/ # 组件库
│ ├─ health/ # 健康管理组件
│ │ ├─ consultation-card/ # 咨询卡片组件
│ │ ├─ health-chart/ # 健康图表组件
│ │ ├─ diet-calendar/ # 饮食日历组件
│ │ ├─ exercise-tracker/ # 运动追踪组件
│ │ ├─ constitution-test/ # 体质测试组件
│ │ ├─ health-indicator/ # 健康指标组件
│ │ └─ medicine-reminder/ # 用药提醒组件
├─ pages/ # 页面
│ |─ health/ # 健康管理页面
│ ├─ index/ # 健康首页
│ ├─ consultation/ # 在线问诊
│ │ ├─ index.vue # 问诊首页
│ │ ├─ doctor-list.vue # 医生列表
│ │ ├─ chat.vue # 咨询聊天
│ │ └─ history.vue # 咨询历史
│ ├─ detection/ # 健康检测
│ │ ├─ index.vue # 检测首页
│ │ ├─ vitals.vue # 生命体征
│ │ ├─ report.vue # 健康报告
│ │ └─ history.vue # 检测历史
│ ├─ chronic/ # 慢病管理
│ │ ├─ index.vue # 慢病首页
│ │ ├─ diabetes.vue # 糖尿病管理
│ │ ├─ hypertension.vue # 高血压管理
│ │ └─ medication.vue # 用药管理
│ ├─ diet/ # 膳食管理
│ │ ├─ index.vue # 膳食首页
│ │ ├─ plan.vue # 膳食计划
│ │ ├─ record.vue # 饮食记录
│ │ └─ analysis.vue # 营养分析
│ ├─ constitution/ # 体质辩识
│ │ ├─ index.vue # 体质首页
│ │ ├─ test.vue # 体质测试
│ │ ├─ result.vue # 测试结果
│ │ └─ advice.vue # 调理建议
│ ├─ exercise/ # 运动管理
│ │ ├─ index.vue # 运动首页
│ │ ├─ plan.vue # 运动计划
│ │ ├─ record.vue # 运动记录
│ │ └─ analysis.vue # 运动分析
│ ├─ encyclopedia/ # 中医百科
│ │ ├─ index.vue # 百科首页
│ │ ├─ herbs.vue # 中药材
│ │ ├─ acupoints.vue # 穴位
│ │ └─ meridians.vue # 经络
│ ├─ education/ # 健康讲堂
│ │ ├─ index.vue # 讲堂首页
│ │ ├─ articles.vue # 文章列表
│ │ ├─ videos.vue # 视频列表
│ │ └─ courses.vue # 课程列表
│ └─ profile/ # 个人档案
│ ├─ index.vue # 档案首页
│ ├─ basic-info.vue # 基本信息
│ ├─ medical-history.vue # 病史信息
│ └─ family-history.vue # 家族史
├─ store/ # 状态管理
│ └─ modules/
│ └─ health/ # 健康管理状态
│ ├─ consultation.ts # 咨询状态
│ ├─ detection.ts # 检测状态
│ ├─ chronic.ts # 慢病状态
│ ├─ diet.ts # 膳食状态
│ ├─ constitution.ts # 体质状态
│ ├─ exercise.ts # 运动状态
│ ├─ encyclopedia.ts # 百科状态
│ ├─ education.ts # 讲堂状态
│ └─ profile.ts # 档案状态
├─ types/ # 类型定义
│ |─ health/ # 健康管理类型
│ ├─ consultation.ts # 咨询类型
│ ├─ detection.ts # 检测类型
│ ├─ chronic.ts # 慢病类型
│ ├─ diet.ts # 膳食类型
│ ├─ constitution.ts # 体质类型
│ ├─ exercise.ts # 运动类型
│ ├─ encyclopedia.ts # 百科类型
│ ├─ education.ts # 讲堂类型
│ └─ profile.ts # 档案类型
├─ utils/ # 工具函数
│ ├─ health/ # 健康工具
│ │ ├─ calculation.ts # 健康计算
│ │ ├─ validation.ts # 数据验证
│ │ ├─ formatter.ts # 数据格式化
│ │ └─ reminder.ts # 提醒管理
│ └─
└─ static/ # 静态资源
├─ health/ # 健康相关图片
├─ icons/ # 图标资源
└─ images/ # 图片资源
```
## 🚀 功能模块详述
### 1. 在线问诊模块
#### 功能特性
- **医生筛选**: 按科室、专业、评分筛选医生
- **多种咨询方式**: 支持文字、语音、视频咨询
- **实时聊天**: WebSocket实现实时通信
- **处方开具**: 医生可开具电子处方
- **咨询记录**: 完整的咨询历史管理
#### 核心功能
```typescript
// 主要API接口
- getDoctors(): 获取医生列表
- createConsultation(): 创建咨询
- sendMessage(): 发送消息
- endConsultation(): 结束咨询
- rateConsultation(): 评价咨询
```
#### 页面流程
```
问诊首页 → 选择医生 → 发起咨询 → 实时聊天 → 咨询结束 → 评价反馈
```
### 2. 健康检测模块
#### 功能特性
- **生命体征监测**: 血压、心率、体温、血氧等
- **健康报告生成**: 基于检测数据自动生成报告
- **趋势分析**: 长期健康数据趋势分析
- **异常预警**: 健康指标异常及时提醒
#### 核心算法
```typescript
// BMI计算
export const calculateBMI = (weight: number, height: number): number => {
const heightInMeters = height / 100;
return Number((weight / (heightInMeters * heightInMeters)).toFixed(1));
};
// 基础代谢率计算
export const calculateBMR = (
weight: number,
height: number,
age: number,
gender: "male" | "female"
): number => {
if (gender === "male") {
return Math.round(88.362 + 13.397 * weight + 4.799 * height - 5.677 * age);
} else {
return Math.round(447.593 + 9.247 * weight + 3.098 * height - 4.33 * age);
}
};
```
### 3. 慢病管理模块
#### 功能特性
- **疾病档案管理**: 糖尿病、高血压等慢病档案
- **数据监测**: 血糖、血压等关键指标监测
- **用药管理**: 药物提醒、用药记录
- **目标管理**: 设置和跟踪健康目标
#### 数据验证
```typescript
// 血糖数据验证
export const validateBloodSugar = (value: number, mealRelation: string) => {
let level: "normal" | "low" | "high" = "normal";
switch (mealRelation) {
case "fasting":
if (value < 3.9) level = "low";
else if (value > 6.1) level = "high";
break;
case "after_meal":
if (value < 3.9) level = "low";
else if (value > 11.1) level = "high";
break;
}
return { level, isValid: value >= 2 && value <= 30 };
};
```
### 4. 膳食管理模块
#### 功能特性
- **营养计算**: 自动计算食物营养成分
- **膳食计划**: 个性化膳食计划制定
- **饮食记录**: 详细的饮食记录和分析
- **食谱推荐**: 基于用户需求的食谱推荐
#### 营养计算
```typescript
// 每日热量需求计算
export const calculateTDEE = (bmr: number, activityLevel: string): number => {
const activityFactors = {
sedentary: 1.2, // 久坐
light: 1.375, // 轻度运动
moderate: 1.55, // 中度运动
active: 1.725, // 重度运动
very_active: 1.9, // 极重度运动
};
return Math.round(bmr * (activityFactors[activityLevel] || 1.2));
};
```
### 5. 体质辩识模块
#### 功能特性
- **中医体质测试**: 标准化体质辩识问卷
- **体质分析**: 详细的体质分析报告
- **调理建议**: 个性化的调理建议
- **体质跟踪**: 体质变化趋势跟踪
#### 体质类型
```typescript
// 九种体质类型
const constitutionTypes = [
"平和质",
"气虚质",
"阳虚质",
"阴虚质",
"痰湿质",
"湿热质",
"血瘀质",
"气郁质",
"特禀质",
];
```
### 6. 运动管理模块
#### 功能特性
- **运动计划**: 个性化运动计划制定
- **运动记录**: 详细的运动数据记录
- **卡路里计算**: 精准的运动消耗计算
- **目标跟踪**: 运动目标设置和跟踪
#### 卡路里计算
```typescript
// 运动消耗卡路里计算
export const calculateCaloriesBurned = (
activity: string,
weight: number,
duration: number
): number => {
const metValues: Record<string, number> = {
walking: 3.5,
running: 8.0,
cycling: 6.0,
swimming: 8.0,
// ...更多运动类型
};
const met = metValues[activity] || 3.0;
return Math.round(met * weight * (duration / 60));
};
```
### 7. 中医百科模块
#### 功能特性
- **中药材查询**: 详细的中药材信息
- **穴位定位**: 穴位位置和功效介绍
- **经络系统**: 经络走向和功能说明
- **中医理论**: 中医基础理论学习
### 8. 健康讲堂模块
#### 功能特性
- **健康文章**: 专业的健康科普文章
- **视频讲座**: 专家健康讲座视频
- **在线课程**: 系统化的健康教育课程
- **学习跟踪**: 学习进度和成果跟踪
### 9. 个人档案模块
#### 功能特性
- **基本信息**: 个人基本健康信息
- **病史管理**: 详细的病史记录
- **家族史**: 家族遗传病史管理
- **健康目标**: 个人健康目标设置
## 📱 界面设计
### 设计原则
#### 1. 用户体验优先
- **简洁直观**: 界面设计简洁明了,操作流程直观
- **响应迅速**: 页面加载和交互响应快速
- **个性化**: 根据用户偏好提供个性化界面
#### 2. 健康主题
- **色彩搭配**: 以绿色、蓝色为主色调,体现健康主题
- **图标设计**: 使用直观的健康相关图标
- **数据可视化**: 通过图表展示健康数据趋势
#### 3. 移动端适配
- **响应式设计**: 适配不同屏幕尺寸
- **触摸友好**: 按钮大小适合手指操作
- **手势支持**: 支持常见的移动端手势操作
### 关键页面设计
#### 健康管理首页
```scss
// 渐变背景设计
.health-dashboard {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
// 卡片式布局
.health-header {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20rpx;
}
}
```
#### 数据可视化
- **环形进度条**: 显示健康评分和目标完成度
- **趋势图表**: 展示健康数据变化趋势
- **状态指示器**: 直观显示健康状态
## 🔧 开发指南
### 环境准备
#### 1. 开发工具安装
```bash
# 安装Node.js (推荐v16+)
node --version
# 安装HBuilderX或配置VS Code
# HBuilderX: https://www.dcloud.io/hbuilderx.html
# VS Code + uni-app插件
# 安装依赖
npm install
# 或
yarn install
```
#### 2. 项目配置
```javascript
// manifest.json配置
{
"name": "健康管理平台",
"appid": "__UNI__XXXXXX",
"description": "专业的移动端健康管理应用",
"versionName": "1.0.0",
"versionCode": "100"
}
```
### 开发规范
#### 1. 代码规范
```typescript
// 组件命名规范
export default defineComponent({
name: "HealthDashboard",
// ...
});
// API接口规范
const HealthAPI = {
getHealthData(): Promise<HealthDataResult> {
return request<HealthDataResult>({
url: "/api/v1/health/data",
method: "GET",
});
},
};
```
#### 2. 样式规范
```scss
// 使用SCSS变量
$primary-color: #007aff;
$success-color: #34c759;
$warning-color: #ff9500;
$danger-color: #ff3b30;
// 组件样式命名
.health-component {
&__header {
}
&__content {
}
&__footer {
}
}
```
#### 3. 类型定义规范
```typescript
// 接口定义
export interface HealthData {
id: string;
userId: string;
measureTime: string;
indicators: HealthIndicator[];
}
// 响应数据格式
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
```
### 状态管理
#### Pinia Store使用
```typescript
// store/modules/health/detection.ts
export const useDetectionStore = defineStore("detection", {
state: () => ({
vitalSigns: [] as VitalSigns[],
loading: false,
error: null as string | null,
}),
getters: {
latestVitalSigns: (state) => state.vitalSigns[0],
healthTrend: (state) => {
// 计算健康趋势逻辑
},
},
actions: {
async fetchVitalSigns() {
this.loading = true;
try {
const response = await DetectionAPI.getVitalSignsHistory();
this.vitalSigns = response.list;
} catch (error) {
this.error = "获取数据失败";
} finally {
this.loading = false;
}
},
},
});
```
### API接口开发
#### 请求封装
```typescript
// utils/request.ts
const request = <T = any>(config: RequestConfig): Promise<T> => {
return new Promise((resolve, reject) => {
uni.request({
url: baseURL + config.url,
method: config.method || "GET",
data: config.data,
header: {
Authorization: getToken(),
"Content-Type": "application/json",
...config.header,
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(new Error(`请求失败: ${res.statusCode}`));
}
},
fail: reject,
});
});
};
```
## 🚀 部署指南
### 多端发布
#### 1. 微信小程序
```bash
# 编译微信小程序
npm run build:mp-weixin
# 上传代码
# 使用微信开发者工具上传
```
#### 2. APP发布
```bash
# Android打包
npm run build:app-android
# iOS打包
npm run build:app-ios
```
#### 3. H5发布
```bash
# H5编译
npm run build:h5
# 部署到服务器
# 将dist/build/h5目录上传到Web服务器
```
### 服务器部署
#### 1. Nginx配置
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
root /var/www/health-app/h5;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend-server:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
#### 2. HTTPS配置
```nginx
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 其他配置...
}
```
## 📊 性能优化
### 1. 代码分割
```typescript
// 路由懒加载
const routes = [
{
path: "/health/consultation",
component: () => import("@/pages/health/consultation/index.vue"),
},
];
```
### 2. 图片优化
```vue
<!-- 使用适当的图片格式和尺寸 -->
<image :src="imageUrl" mode="aspectFill" lazy-load @error="handleImageError" />
```
### 3. 数据缓存
```typescript
// 使用Pinia持久化
import { createPinia } from "pinia";
import { createPersistedState } from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(createPersistedState());
```
## 🔒 安全措施
### 1. 数据加密
```typescript
// 敏感数据加密存储
const encryptData = (data: string): string => {
// 使用AES等加密算法
return encrypt(data, SECRET_KEY);
};
```
### 2. API安全
```typescript
// JWT Token验证
const getAuthHeader = (): Record<string, string> => {
const token = getToken();
return token ? { Authorization: `Bearer ${token}` } : {};
};
```
### 3. 输入验证
```typescript
// 严格的输入验证
export const validateHealthData = (data: any): ValidationResult => {
const errors: string[] = [];
if (!data.bloodPressure || !isValidBloodPressure(data.bloodPressure)) {
errors.push("血压数据格式错误");
}
return {
isValid: errors.length === 0,
errors,
};
};
```
## 🧪 测试策略
### 1. 单元测试
```typescript
// 健康计算函数测试
describe("Health Calculations", () => {
test("BMI calculation should be correct", () => {
const bmi = calculateBMI(70, 175);
expect(bmi).toBe(22.9);
});
});
```
### 2. 集成测试
```typescript
// API接口测试
describe("Health API", () => {
test("should fetch vital signs data", async () => {
const data = await DetectionAPI.getVitalSignsHistory({
page: 1,
pageSize: 10,
});
expect(data.list).toBeDefined();
});
});
```
### 3. E2E测试
```typescript
// 用户流程测试
describe("Health Management Flow", () => {
test("user can record vital signs", async () => {
// 模拟用户操作流程
});
});
```
## 📈 监控与分析
### 1. 错误监控
```typescript
// 全局错误处理
uni.onError((error) => {
console.error("应用错误:", error);
// 上报错误信息到监控平台
reportError(error);
});
```
### 2. 性能监控
```typescript
// 页面性能监控
const performanceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log("性能指标:", entry);
});
});
```
### 3. 用户行为分析
```typescript
// 埋点统计
const trackUserAction = (action: string, params: any) => {
// 发送用户行为数据
analytics.track(action, params);
};
```
## 🔮 扩展功能
### 1. AI智能推荐
- **健康风险评估**: 基于AI模型评估用户健康风险
- **个性化建议**: 机器学习生成个性化健康建议
- **疾病预测**: 基于历史数据预测疾病风险
### 2. IoT设备集成
- **智能硬件对接**: 支持智能手环、血压计等设备
- **数据自动同步**: 设备数据自动同步到应用
- **实时监控**: 24小时健康数据监控
### 3. 社交健康
- **健康社区**: 用户健康经验分享社区
- **家庭健康**: 家庭成员健康管理
- **健康挑战**: 健康目标挑战活动
### 4. 医疗服务扩展
- **预约挂号**: 在线预约医院挂号
- **体检预约**: 体检项目预约和管理
- **健康保险**: 健康保险产品推荐
## ❓ 常见问题
### Q1: 如何处理跨平台兼容性问题?
**A**: 使用uni-app的条件编译功能针对不同平台编写特定代码
```vue
<!-- #ifdef MP-WEIXIN -->
<!-- 微信小程序特有功能 -->
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<!-- APP特有功能 -->
<!-- #endif -->
```
### Q2: 健康数据的隐私保护如何处理?
**A**:
1. 数据加密存储和传输
2. 用户隐私权限控制
3. 数据访问日志记录
4. 符合GDPR等隐私法规
### Q3: 如何保证健康数据的准确性?
**A**:
1. 多重数据验证机制
2. 异常数据检测和提醒
3. 专业医疗团队审核
4. 用户反馈和纠错机制
### Q4: 离线功能如何实现?
**A**:
1. 关键数据本地缓存
2. 离线操作队列管理
3. 网络恢复后数据同步
4. 离线提醒功能
### Q5: 如何处理大量健康数据?
**A**:
1. 数据分页加载
2. 虚拟滚动优化
3. 数据压缩和缓存
4. 后台数据清理
更新日志\*\*: [更新日志地址]
### 开源协议
本项目采用 MIT 开源协议,欢迎贡献代码和提出改进建议。
---
**版本**: v1.0.0
**更新时间**: 2024年12月
**文档维护**: 开发团队

88
commitlint.config.cjs Normal file
View File

@ -0,0 +1,88 @@
module.exports = {
// 继承的规则
extends: ["@commitlint/config-conventional"],
// 自定义规则
rules: {
// 提交类型枚举git提交type必须是以下类型 @see https://commitlint.js.org/#/reference-rules
"type-enum": [
2,
"always",
[
"feat", // 新增功能
"fix", // 修复缺陷
"docs", // 文档变更
"style", // 代码格式(不影响功能,例如空格、分号等格式修正)
"refactor", // 代码重构(不包括 bug 修复、功能新增)
"perf", // 性能优化
"test", // 添加疏漏测试或已有测试改动
"build", // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
"ci", // 修改 CI 配置、脚本
"revert", // 回滚 commit
"chore", // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
],
],
"subject-case": [0], // subject大小写不做校验
},
prompt: {
messages: {
type: "选择你要提交的类型 :",
scope: "选择一个提交范围(可选):",
customScope: "请输入自定义的提交范围 :",
subject: "填写简短精炼的变更描述 :\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: "选择关联issue前缀可选:",
customFooterPrefix: "输入自定义issue前缀 :",
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
generatingByAI: "正在通过 AI 生成你的提交简短描述...",
generatedSelectByAI: "选择一个 AI 生成的简短描述:",
confirmCommit: "是否提交或修改commit ?",
},
// prettier-ignore
types: [
{ value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" },
{ value: "fix", name: "修复: 🐛 修复缺陷", emoji: ":bug:" },
{ value: "docs", name: "文档: 📝 文档变更", emoji: ":memo:" },
{ value: "style", name: "格式: 💄 代码格式(不影响功能,例如空格、分号等格式修正)", emoji: ":lipstick:" },
{ value: "refactor", name: "重构: ♻️ 代码重构(不包括 bug 修复、功能新增)", emoji: ":recycle:" },
{ value: "perf", name: "性能: ⚡️ 性能优化", emoji: ":zap:" },
{ value: "test", name: "测试: ✅ 添加疏漏测试或已有测试改动", emoji: ":white_check_mark:"},
{ value: "build", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)", emoji: ":package:"},
{ value: "ci", name: "集成: 🎡 修改 CI 配置、脚本", emoji: ":ferris_wheel:"},
{ value: "revert", name: "回退: ⏪️ 回滚 commit",emoji: ":rewind:"},
{ value: "chore", name: "其他: 🔨 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: ":hammer:"},
],
useEmoji: true,
emojiAlign: "center",
useAI: false,
aiNumber: 1,
themeColorCode: "",
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: "bottom",
customScopesAlias: "custom",
emptyScopesAlias: "empty",
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ["feat", "fix"],
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
issuePrefixes: [{ value: "closed", name: "closed: ISSUES has been processed" }],
customIssuePrefixAlign: "top",
emptyIssuePrefixAlias: "skip",
customIssuePrefixAlias: "custom",
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: "",
},
};

129
eslint.config.mjs Normal file
View File

@ -0,0 +1,129 @@
// https://eslint.nodejs.cn/docs/latest/use/configure/configuration-files
import globals from "globals";
import pluginJs from "@eslint/js"; // JavaScript 规则
import pluginVue from "eslint-plugin-vue"; // Vue 规则
import pluginTypeScript from "@typescript-eslint/eslint-plugin"; // TypeScript 规则
import parserVue from "vue-eslint-parser"; // Vue 解析器
import parserTypeScript from "@typescript-eslint/parser"; // TypeScript 解析器
import configPrettier from "eslint-config-prettier"; // 禁用与 Prettier 冲突的规则
import pluginPrettier from "eslint-plugin-prettier"; // 运行 Prettier 规则
// 解析自动导入配置
import fs from "fs";
const autoImportConfig = JSON.parse(fs.readFileSync(".eslintrc-auto-import.json", "utf-8"));
/** @type {import('eslint').Linter.Config[]} */
export default [
// 忽略指定文件
{
ignores: [
"node_modules/**",
"dist/**",
"unpackage/**",
"public/**",
"static/**",
"**/u-charts/**",
"**/qiun-**/**",
"**/auto-imports.d.ts",
"src/types/auto-imports.d.ts",
],
},
// 检查文件的配置
{
files: ["**/*.{js,mjs,cjs,ts,vue}"],
ignores: ["**/*.d.ts"],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...autoImportConfig.globals,
...{
uni: "readonly", // uni-app 全局对象
UniApp: "readonly", // uni-app 全局对象
ResponseData: "readonly", // 统一响应数据类型
PageResult: "readonly", // 分页结果数据类型
PageQuery: "readonly", // 分页查询数据类型
OptionType: "readonly", // 选项类型
getCurrentPages: "readonly", // uni-app 全局 API
},
},
},
plugins: { prettier: pluginPrettier },
rules: {
...configPrettier.rules, // 关闭与 Prettier 冲突的规则
...pluginPrettier.configs.recommended.rules, // 启用 Prettier 规则
"prettier/prettier": "error", // 强制 Prettier 格式化
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_", // 忽略参数名以 _ 开头的参数未使用警告
varsIgnorePattern: "^[A-Z0-9_]+$", // 忽略变量名为大写字母、数字或下划线组合的未使用警告(枚举定义未使用场景)
ignoreRestSiblings: true, // 忽略解构赋值中同级未使用变量的警告
},
],
"no-prototype-builtins": "off", // 允许直接调用Object.prototype方法
"no-constant-binary-expression": "warn", // 将常量二元表达式警告降为警告级别
},
},
// JavaScript 配置
pluginJs.configs.recommended,
// TypeScript 配置
{
files: ["**/*.ts"],
ignores: ["**/*.d.ts"], // 排除d.ts文件
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module",
},
},
plugins: { "@typescript-eslint": pluginTypeScript },
rules: {
...pluginTypeScript.configs.strict.rules, // TypeScript 严格规则
"@typescript-eslint/no-explicit-any": "off", // 允许使用 any
"@typescript-eslint/no-empty-function": "off", // 允许空函数
"@typescript-eslint/no-empty-object-type": "off", // 允许空对象类型
},
},
// Vue 配置
{
files: ["**/*.vue"],
languageOptions: {
parser: parserVue,
parserOptions: {
parser: parserTypeScript,
sourceType: "module",
},
},
plugins: { vue: pluginVue, "@typescript-eslint": pluginTypeScript },
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs["vue3-recommended"].rules, // Vue 3 推荐规则
"vue/no-v-html": "off", // 允许 v-html
"vue/multi-word-component-names": "off", // 允许单个单词组件名
},
},
// 单独针对第三方组件的处理
{
files: ["**/qiun-data-charts.vue", "**/qiun-data-charts/**/*.vue", "**/u-charts/**"],
linterOptions: {
noInlineConfig: false,
reportUnusedDisableDirectives: false,
},
rules: {
// 禁用所有规则
...Object.fromEntries(
Object.keys(pluginVue.configs["vue3-recommended"].rules || {}).map((key) => [key, "off"])
),
"no-unused-vars": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-explicit-any": "off",
},
},
];

24
index.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
"CSS" in window &&
typeof CSS.supports === "function" &&
(CSS.supports("top: env(a)") || CSS.supports("top: constant(a)"));
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ", viewport-fit=cover" : "") +
'" />'
);
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

142
package.json Normal file
View File

@ -0,0 +1,142 @@
{
"name": "vue-uniapp-template",
"version": "0.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:app-harmony": "uni -p app-harmony",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:app-harmony": "uni build -p app-harmony",
"build:custom": "uni build -p",
"build:h5": "uni build --mode production",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit",
"lint:eslint": "eslint \"src/**/*.{vue,ts}\" --fix",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint \"**/*.{css,scss,vue,html}\" --fix",
"lint:lint-staged": "lint-staged",
"prepare": "husky",
"commit": "git-cz"
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write"
],
"*.{cjs,json}": [
"prettier --write"
],
"*.{vue,html}": [
"eslint --fix",
"prettier --write",
"stylelint --fix"
],
"*.{scss,css}": [
"stylelint --fix",
"prettier --write"
],
"*.md": [
"prettier --write"
]
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4060620250520001",
"@dcloudio/uni-app-harmony": "3.0.0-4060620250520001",
"@dcloudio/uni-app-plus": "3.0.0-4060620250520001",
"@dcloudio/uni-components": "3.0.0-4060620250520001",
"@dcloudio/uni-h5": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-harmony": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-jd": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-lark": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-qq": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
"pinia": "^2.2.2",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9",
"wot-design-uni": "^1.4.0"
},
"devDependencies": {
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
"@eslint/js": "^9.10.0",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0",
"@vue/runtime-core": "^3.4.21",
"commitizen": "^4.3.0",
"cz-git": "^1.9.4",
"eslint": "^9.10.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.28.0",
"globals": "^15.9.0",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.3.3",
"sass": "1.78.0",
"sass-loader": "^16.0.2",
"stylelint": "^16.9.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^5.1.0",
"stylelint-config-recommended": "^14.0.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-prettier": "^5.0.2",
"typescript": "^4.9.4",
"typescript-eslint": "^8.6.0",
"unocss": "^0.62.4",
"unocss-preset-weapp": "^0.62.2",
"unplugin-auto-import": "^0.18.3",
"vite": "6.3.4",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^1.0.24"
}
}

8
shims-uni.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types='@dcloudio/types' />
import "vue";
declare module "@vue/runtime-core" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

44
src/App.vue Normal file
View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { useThemeStore } from "@/store";
// 主题初始化
const themeStore = useThemeStore();
onLaunch(() => {
console.log("App Launch");
// 初始化主题
themeStore.initTheme();
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
</script>
<style lang="scss">
/* H5 环境样式变量设置 */
:root {
--primary-color: #165dff;
--primary-color-light: #94bfff;
--primary-color-dark: #0e3c9b;
}
/* 小程序环境样式变量设置 */
page {
--primary-color: #165dff;
--primary-color-light: #94bfff;
--primary-color-dark: #0e3c9b;
background: #f8f8f8;
}
/* 动态加载小程序主题色的钩子 */
/* 用于通过小程序原生API获取主题色并应用 */
.theme-container {
display: none;
}
</style>

63
src/api/auth/index.ts Normal file
View File

@ -0,0 +1,63 @@
import request from "@/utils/request";
const AuthAPI = {
/**
* 登录接口
*
* @returns 返回 token
* @param data
*/
login(data: LoginFormData): Promise<LoginResult> {
console.log("data", data);
return request<LoginResult>({
url: "/api/v1/auth/login",
method: "POST",
data: data,
header: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
},
/**
* 微信登录接口
*
* @param code 微信登录code
* @returns 返回 token
*/
wechatLogin(code: string): Promise<LoginResult> {
return request<LoginResult>({
url: "/api/v1/auth/wechat-login",
method: "POST",
data: { code },
header: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
},
/**
* 登出接口
*/
logout(): Promise<void> {
return request({
url: "/api/v1/auth/logout",
method: "DELETE",
});
},
};
export default AuthAPI;
/** 登录响应 */
export interface LoginResult {
/** 访问token */
accessToken: string;
/** token 类型 */
tokenType?: string;
}
export interface LoginFormData {
username: string;
password: string;
}

75
src/api/file/index.ts Normal file
View File

@ -0,0 +1,75 @@
import { getToken } from "@/utils/cache";
import { ResultCodeEnum } from "@/enums/ResultCodeEnum";
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径
let baseApi = import.meta.env.VITE_APP_API_URL;
// #ifdef H5
baseApi = import.meta.env.VITE_APP_BASE_API;
// #endif
const FileAPI = {
/**
* 文件上传地址
*/
uploadUrl: baseApi + "/api/v1/files",
/**
* 上传文件
*
* @param filePath
*/
upload(filePath: string): Promise<FileInfo> {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: this.uploadUrl,
filePath: filePath,
name: "file",
header: {
Authorization: getToken() ? `Bearer ${getToken()}` : "",
},
formData: {},
success: (response) => {
const resData = JSON.parse(response.data) as ResponseData<FileInfo>;
// 业务状态码 00000 表示成功
if (resData.code === ResultCodeEnum.SUCCESS) {
resolve(resData.data);
} else {
// 其他业务处理失败
uni.showToast({
title: resData.msg || "文件上传失败",
icon: "none",
});
reject({
message: resData.msg || "业务处理失败",
code: resData.code,
});
}
},
fail: (error) => {
console.log("fail error", error);
uni.showToast({
title: "文件上传请求失败",
icon: "none",
duration: 2000,
});
reject({
message: "文件上传请求失败",
error,
});
},
});
});
},
};
export default FileAPI;
/**
* 文件API类型声明
*/
export interface FileInfo {
/** 文件名 */
name: string;
/** 文件路径 */
url: string;
}

295
src/api/health/chronic.ts Normal file
View File

@ -0,0 +1,295 @@
// api/health/chronic.ts
import request from "@/utils/request";
const ChronicAPI = {
/**
* 获取慢性疾病列表
*
* @returns 慢性疾病列表
*/
getChronicDiseases(): Promise<ChronicDiseaseListResult> {
return request<ChronicDiseaseListResult>({
url: "/api/v1/health/chronic/diseases",
method: "GET",
});
},
/**
* 添加慢性疾病
*
* @param data 疾病数据
* @returns 疾病信息
*/
addChronicDisease(data: ChronicDiseaseData): Promise<ChronicDiseaseResult> {
return request<ChronicDiseaseResult>({
url: "/api/v1/health/chronic/diseases",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 更新慢性疾病
*
* @param diseaseId 疾病ID
* @param data 更新数据
* @returns 疾病信息
*/
updateChronicDisease(
diseaseId: string,
data: Partial<ChronicDiseaseData>
): Promise<ChronicDiseaseResult> {
return request<ChronicDiseaseResult>({
url: `/api/v1/health/chronic/diseases/${diseaseId}`,
method: "PUT",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 记录血糖
*
* @param data 血糖数据
* @returns 血糖记录
*/
recordBloodSugar(data: BloodSugarData): Promise<BloodSugarResult> {
return request<BloodSugarResult>({
url: "/api/v1/health/chronic/blood-sugar",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取血糖记录
*
* @param params 查询参数
* @returns 血糖记录列表
*/
getBloodSugarRecords(params: BloodSugarQueryParams): Promise<BloodSugarListResult> {
return request<BloodSugarListResult>({
url: "/api/v1/health/chronic/blood-sugar/history",
method: "GET",
data: params,
});
},
/**
* 记录血压
*
* @param data 血压数据
* @returns 血压记录
*/
recordBloodPressure(data: BloodPressureData): Promise<BloodPressureResult> {
return request<BloodPressureResult>({
url: "/api/v1/health/chronic/blood-pressure",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取血压记录
*
* @param params 查询参数
* @returns 血压记录列表
*/
getBloodPressureRecords(params: BloodPressureQueryParams): Promise<BloodPressureListResult> {
return request<BloodPressureListResult>({
url: "/api/v1/health/chronic/blood-pressure/history",
method: "GET",
data: params,
});
},
/**
* 获取用药提醒
*
* @returns 用药提醒列表
*/
getMedicationReminders(): Promise<MedicationReminderListResult> {
return request<MedicationReminderListResult>({
url: "/api/v1/health/chronic/medication/reminders",
method: "GET",
});
},
/**
* 更新用药状态
*
* @param medicationId 药物ID
* @param data 状态数据
*/
updateMedicationStatus(medicationId: string, data: MedicationStatusData): Promise<void> {
return request({
url: `/api/v1/health/chronic/medication/${medicationId}/status`,
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
};
export default ChronicAPI;
/** 慢性疾病数据 */
export interface ChronicDiseaseData {
name: string;
type: "diabetes" | "hypertension" | "heart_disease" | "other";
diagnosisDate: string;
severity: "mild" | "moderate" | "severe";
currentStatus: "stable" | "improving" | "worsening";
medications: MedicationData[];
targetValues: Record<string, number>;
}
/** 慢性疾病信息 */
export interface ChronicDisease {
id: string;
name: string;
type: "diabetes" | "hypertension" | "heart_disease" | "other";
diagnosisDate: string;
severity: "mild" | "moderate" | "severe";
currentStatus: "stable" | "improving" | "worsening";
medications: Medication[];
targetValues: Record<string, number>;
}
/** 药物数据 */
export interface MedicationData {
name: string;
type: string;
dosage: string;
frequency: string;
startDate: string;
endDate?: string;
reminders: boolean;
sideEffects?: string[];
}
/** 药物信息 */
export interface Medication {
id: string;
name: string;
type: string;
dosage: string;
frequency: string;
startDate: string;
endDate?: string;
reminders: boolean;
sideEffects?: string[];
}
/** 慢性疾病列表响应 */
export interface ChronicDiseaseListResult {
list: ChronicDisease[];
}
/** 慢性疾病响应 */
export interface ChronicDiseaseResult {
disease: ChronicDisease;
}
/** 血糖数据 */
export interface BloodSugarData {
value: number;
measureTime: string;
mealRelation: "fasting" | "before_meal" | "after_meal" | "bedtime";
notes?: string;
}
/** 血糖记录 */
export interface BloodSugarRecord {
id: string;
userId: string;
value: number;
measureTime: string;
mealRelation: "fasting" | "before_meal" | "after_meal" | "bedtime";
notes?: string;
}
/** 血糖响应 */
export interface BloodSugarResult {
record: BloodSugarRecord;
}
/** 血糖查询参数 */
export interface BloodSugarQueryParams {
startDate?: string;
endDate?: string;
page: number;
pageSize: number;
}
/** 血糖列表响应 */
export interface BloodSugarListResult {
list: BloodSugarRecord[];
total: number;
page: number;
pageSize: number;
}
/** 血压数据 */
export interface BloodPressureData {
systolic: number;
diastolic: number;
heartRate: number;
measureTime: string;
notes?: string;
}
/** 血压记录 */
export interface BloodPressureRecord {
id: string;
userId: string;
systolic: number;
diastolic: number;
heartRate: number;
measureTime: string;
notes?: string;
}
/** 血压响应 */
export interface BloodPressureResult {
record: BloodPressureRecord;
}
/** 血压查询参数 */
export interface BloodPressureQueryParams {
startDate?: string;
endDate?: string;
page: number;
pageSize: number;
}
/** 血压列表响应 */
export interface BloodPressureListResult {
list: BloodPressureRecord[];
total: number;
page: number;
pageSize: number;
}
/** 用药提醒列表响应 */
export interface MedicationReminderListResult {
list: Medication[];
}
/** 用药状态数据 */
export interface MedicationStatusData {
taken: boolean;
}

View File

@ -0,0 +1,163 @@
import request from "@/utils/request";
const ConstitutionAPI = {
/**
* 获取体质类型
*
* @returns 体质类型列表
*/
getConstitutionTypes(): Promise<ConstitutionTypeListResult> {
return request<ConstitutionTypeListResult>({
url: "/api/v1/health/constitution/types",
method: "GET",
});
},
/**
* 获取体质测试题目
*
* @returns 测试题目列表
*/
getConstitutionQuestions(): Promise<ConstitutionQuestionListResult> {
return request<ConstitutionQuestionListResult>({
url: "/api/v1/health/constitution/questions",
method: "GET",
});
},
/**
* 提交体质测试
*
* @param data 测试答案
* @returns 测试结果
*/
submitConstitutionTest(data: ConstitutionTestData): Promise<ConstitutionTestResult> {
return request<ConstitutionTestResult>({
url: "/api/v1/health/constitution/test",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取体质测试历史
*
* @returns 测试历史列表
*/
getConstitutionTestHistory(): Promise<ConstitutionTestHistoryResult> {
return request<ConstitutionTestHistoryResult>({
url: "/api/v1/health/constitution/test/history",
method: "GET",
});
},
/**
* 获取体质建议
*
* @param constitutionType 体质类型
* @returns 体质建议
*/
getConstitutionAdvice(constitutionType: string): Promise<ConstitutionAdviceResult> {
return request<ConstitutionAdviceResult>({
url: `/api/v1/health/constitution/advice/${constitutionType}`,
method: "GET",
});
},
};
export default ConstitutionAPI;
/** 体质类型列表响应 */
export interface ConstitutionTypeListResult {
list: ConstitutionType[];
}
/** 体质类型 */
export interface ConstitutionType {
id: string;
name: string;
description: string;
characteristics: string[];
healthRisks: string[];
dietAdvice: string[];
exerciseAdvice: string[];
lifestyleAdvice: string[];
}
/** 体质测试题目列表响应 */
export interface ConstitutionQuestionListResult {
list: ConstitutionQuestion[];
}
/** 体质测试题目 */
export interface ConstitutionQuestion {
id: string;
question: string;
options: ConstitutionOption[];
category: string;
}
/** 体质测试选项 */
export interface ConstitutionOption {
id: string;
text: string;
score: number;
constitutionType: string;
}
/** 体质测试数据 */
export interface ConstitutionTestData {
answers: Record<string, string>;
}
/** 体质测试响应 */
export interface ConstitutionTestResult {
test: ConstitutionTest;
}
/** 体质测试 */
export interface ConstitutionTest {
id: string;
userId: string;
questions: ConstitutionQuestion[];
result: ConstitutionResult;
testDate: string;
}
/** 体质测试结果 */
export interface ConstitutionResult {
primaryType: string;
secondaryType?: string;
scores: Record<string, number>;
recommendations: ConstitutionRecommendation[];
}
/** 体质建议 */
export interface ConstitutionRecommendation {
category: "diet" | "exercise" | "lifestyle" | "emotion";
title: string;
content: string;
priority: "high" | "medium" | "low";
}
/** 体质测试历史响应 */
export interface ConstitutionTestHistoryResult {
list: ConstitutionTest[];
}
/** 体质建议响应 */
export interface ConstitutionAdviceResult {
advice: ConstitutionAdvice;
}
/** 体质建议详情 */
export interface ConstitutionAdvice {
constitutionType: ConstitutionType;
recommendations: ConstitutionRecommendation[];
dietPlan: string[];
exercisePlan: string[];
lifestyleTips: string[];
}

View File

@ -0,0 +1,277 @@
import request from "@/utils/request";
const ConsultationAPI = {
/**
* 获取医生列表
*
* @param params 查询参数
* @returns 医生列表
*/
getDoctors(params: DoctorQueryParams): Promise<DoctorListResult> {
return request<DoctorListResult>({
url: "/api/v1/health/consultation/doctors",
method: "GET",
data: params,
});
},
/**
* 获取医生详情
*
* @param doctorId 医生ID
* @returns 医生详情
*/
getDoctorDetail(doctorId: string): Promise<DoctorDetailResult> {
return request<DoctorDetailResult>({
url: `/api/v1/health/consultation/doctors/${doctorId}`,
method: "GET",
});
},
/**
* 创建咨询
*
* @param data 咨询数据
* @returns 咨询信息
*/
createConsultation(data: CreateConsultationData): Promise<ConsultationResult> {
return request<ConsultationResult>({
url: "/api/v1/health/consultation/create",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取咨询历史
*
* @param params 查询参数
* @returns 咨询历史列表
*/
getConsultationHistory(params: ConsultationQueryParams): Promise<ConsultationListResult> {
return request<ConsultationListResult>({
url: "/api/v1/health/consultation/history",
method: "GET",
data: params,
});
},
/**
* 获取咨询详情
*
* @param consultationId 咨询ID
* @returns 咨询详情
*/
getConsultationDetail(consultationId: string): Promise<ConsultationDetailResult> {
return request<ConsultationDetailResult>({
url: `/api/v1/health/consultation/${consultationId}`,
method: "GET",
});
},
/**
* 发送消息
*
* @param data 消息数据
* @returns 消息信息
*/
sendMessage(data: SendMessageData): Promise<MessageResult> {
return request<MessageResult>({
url: "/api/v1/health/consultation/message",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取聊天记录
*
* @param consultationId 咨询ID
* @param params 查询参数
* @returns 聊天记录列表
*/
getChatMessages(consultationId: string, params: MessageQueryParams): Promise<MessageListResult> {
return request<MessageListResult>({
url: `/api/v1/health/consultation/${consultationId}/messages`,
method: "GET",
data: params,
});
},
/**
* 结束咨询
*
* @param consultationId 咨询ID
*/
endConsultation(consultationId: string): Promise<void> {
return request({
url: `/api/v1/health/consultation/${consultationId}/end`,
method: "POST",
});
},
/**
* 评价咨询
*
* @param consultationId 咨询ID
* @param data 评价数据
*/
rateConsultation(consultationId: string, data: RateConsultationData): Promise<void> {
return request({
url: `/api/v1/health/consultation/${consultationId}/rate`,
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
};
export default ConsultationAPI;
/** 医生查询参数 */
export interface DoctorQueryParams {
specialty?: string;
isOnline?: boolean;
page: number;
pageSize: number;
}
/** 医生列表响应 */
export interface DoctorListResult {
list: Doctor[];
total: number;
page: number;
pageSize: number;
}
/** 医生详情响应 */
export interface DoctorDetailResult {
doctor: Doctor;
}
/** 医生信息 */
export interface Doctor {
id: string;
name: string;
avatar: string;
title: string;
department: string;
hospital: string;
experience: number;
rating: number;
consultationFee: number;
isOnline: boolean;
specialty: string[];
introduction: string;
}
/** 创建咨询数据 */
export interface CreateConsultationData {
doctorId: string;
type: "text" | "voice" | "video";
symptoms: string;
}
/** 咨询响应 */
export interface ConsultationResult {
consultation: Consultation;
}
/** 咨询信息 */
export interface Consultation {
id: string;
doctorId: string;
patientId: string;
type: "text" | "voice" | "video";
status: "pending" | "ongoing" | "completed" | "cancelled";
symptoms: string;
diagnosis?: string;
prescription?: Prescription[];
startTime: string;
endTime?: string;
fee: number;
rating?: number;
review?: string;
}
/** 处方信息 */
export interface Prescription {
id: string;
medicineId: string;
medicineName: string;
dosage: string;
frequency: string;
duration: string;
notes?: string;
}
/** 咨询查询参数 */
export interface ConsultationQueryParams {
page: number;
pageSize: number;
}
/** 咨询列表响应 */
export interface ConsultationListResult {
list: Consultation[];
total: number;
page: number;
pageSize: number;
}
/** 咨询详情响应 */
export interface ConsultationDetailResult {
consultation: Consultation;
}
/** 发送消息数据 */
export interface SendMessageData {
consultationId: string;
type: "text" | "image" | "voice" | "video" | "prescription";
content: string;
}
/** 消息响应 */
export interface MessageResult {
message: ChatMessage;
}
/** 聊天消息 */
export interface ChatMessage {
id: string;
consultationId: string;
senderId: string;
senderType: "doctor" | "patient";
type: "text" | "image" | "voice" | "video" | "prescription";
content: string;
timestamp: string;
isRead: boolean;
}
/** 消息查询参数 */
export interface MessageQueryParams {
page: number;
pageSize: number;
}
/** 消息列表响应 */
export interface MessageListResult {
list: ChatMessage[];
total: number;
page: number;
pageSize: number;
}
/** 评价咨询数据 */
export interface RateConsultationData {
rating: number;
review: string;
}

212
src/api/health/detection.ts Normal file
View File

@ -0,0 +1,212 @@
import request from "@/utils/request";
const DetectionAPI = {
/**
* 记录生命体征
*
* @param data 生命体征数据
* @returns 记录结果
*/
recordVitalSigns(data: VitalSignsData): Promise<VitalSignsResult> {
return request<VitalSignsResult>({
url: "/api/v1/health/detection/vitals",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取生命体征历史
*
* @param params 查询参数
* @returns 生命体征历史列表
*/
getVitalSignsHistory(params: VitalSignsQueryParams): Promise<VitalSignsListResult> {
return request<VitalSignsListResult>({
url: "/api/v1/health/detection/vitals/history",
method: "GET",
data: params,
});
},
/**
* 生成健康报告
*
* @param data 报告类型
* @returns 健康报告
*/
generateHealthReport(data: GenerateReportData): Promise<HealthReportResult> {
return request<HealthReportResult>({
url: "/api/v1/health/detection/report/generate",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取健康报告
*
* @param params 查询参数
* @returns 健康报告列表
*/
getHealthReports(params: ReportQueryParams): Promise<HealthReportListResult> {
return request<HealthReportListResult>({
url: "/api/v1/health/detection/reports",
method: "GET",
data: params,
});
},
/**
* 获取健康报告详情
*
* @param reportId 报告ID
* @returns 健康报告详情
*/
getHealthReportDetail(reportId: string): Promise<HealthReportDetailResult> {
return request<HealthReportDetailResult>({
url: `/api/v1/health/detection/reports/${reportId}`,
method: "GET",
});
},
/**
* 获取健康趋势
*
* @param params 查询参数
* @returns 健康趋势数据
*/
getHealthTrends(params: TrendQueryParams): Promise<HealthTrendResult> {
return request<HealthTrendResult>({
url: "/api/v1/health/detection/trends",
method: "GET",
data: params,
});
},
};
export default DetectionAPI;
/** 生命体征数据 */
export interface VitalSignsData {
bloodPressure: {
systolic: number;
diastolic: number;
};
heartRate: number;
bodyTemperature: number;
bloodOxygen: number;
bloodGlucose: number;
weight: number;
height: number;
steps: number;
deviceId?: string;
}
/** 生命体征响应 */
export interface VitalSignsResult {
vitalSigns: VitalSigns;
}
/** 生命体征信息 */
export interface VitalSigns {
id: string;
userId: string;
bloodPressure: {
systolic: number;
diastolic: number;
};
heartRate: number;
bodyTemperature: number;
bloodOxygen: number;
weight: number;
height: number;
bmi: number;
recordTime: string;
deviceId?: string;
}
/** 生命体征查询参数 */
export interface VitalSignsQueryParams {
startDate?: string;
endDate?: string;
page: number;
pageSize: number;
}
/** 生命体征列表响应 */
export interface VitalSignsListResult {
list: VitalSigns[];
total: number;
page: number;
pageSize: number;
}
/** 生成报告数据 */
export interface GenerateReportData {
type: "basic" | "comprehensive" | "specialized";
}
/** 健康报告响应 */
export interface HealthReportResult {
report: HealthReport;
}
/** 健康报告信息 */
export interface HealthReport {
id: string;
userId: string;
type: "basic" | "comprehensive" | "specialized";
indicators: HealthIndicator[];
overallScore: number;
riskLevel: "low" | "medium" | "high";
suggestions: string[];
reportDate: string;
nextCheckDate?: string;
}
/** 健康指标 */
export interface HealthIndicator {
name: string;
value: number;
unit: string;
normalRange: [number, number];
status: "normal" | "warning" | "abnormal";
trend: "up" | "down" | "stable";
}
/** 报告查询参数 */
export interface ReportQueryParams {
page: number;
pageSize: number;
}
/** 健康报告列表响应 */
export interface HealthReportListResult {
list: HealthReport[];
total: number;
page: number;
pageSize: number;
}
/** 健康报告详情响应 */
export interface HealthReportDetailResult {
report: HealthReport;
}
/** 趋势查询参数 */
export interface TrendQueryParams {
indicator: string;
period: "7d" | "30d" | "90d" | "1y";
}
/** 健康趋势响应 */
export interface HealthTrendResult {
trends: HealthIndicator[];
}

329
src/api/health/diet.ts Normal file
View File

@ -0,0 +1,329 @@
import request from "@/utils/request";
const DietAPI = {
/**
* 搜索食物
*
* @param keyword 关键词
* @param params 查询参数
* @returns 食物列表
*/
searchFoods(keyword: string, params: FoodQueryParams): Promise<FoodListResult> {
return request<FoodListResult>({
url: "/api/v1/health/diet/foods/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 获取食物详情
*
* @param foodId 食物ID
* @returns 食物详情
*/
getFoodDetail(foodId: string): Promise<FoodDetailResult> {
return request<FoodDetailResult>({
url: `/api/v1/health/diet/foods/${foodId}`,
method: "GET",
});
},
/**
* 记录用餐
*
* @param data 用餐数据
* @returns 用餐记录
*/
recordMeal(data: MealRecordData): Promise<MealRecordResult> {
return request<MealRecordResult>({
url: "/api/v1/health/diet/meals",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取用餐记录
*
* @param params 查询参数
* @returns 用餐记录列表
*/
getMealRecords(params: MealRecordQueryParams): Promise<MealRecordListResult> {
return request<MealRecordListResult>({
url: "/api/v1/health/diet/meals/history",
method: "GET",
data: params,
});
},
/**
* 获取营养分析
*
* @param params 查询参数
* @returns 营养分析结果
*/
getNutritionAnalysis(params: NutritionAnalysisParams): Promise<NutritionAnalysisResult> {
return request<NutritionAnalysisResult>({
url: "/api/v1/health/diet/nutrition/analysis",
method: "GET",
data: params,
});
},
/**
* 获取膳食计划
*
* @returns 膳食计划列表
*/
getDietPlans(): Promise<DietPlanListResult> {
return request<DietPlanListResult>({
url: "/api/v1/health/diet/plans",
method: "GET",
});
},
/**
* 创建膳食计划
*
* @param data 膳食计划数据
* @returns 膳食计划
*/
createDietPlan(data: DietPlanData): Promise<DietPlanResult> {
return request<DietPlanResult>({
url: "/api/v1/health/diet/plans",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 激活膳食计划
*
* @param planId 计划ID
*/
activateDietPlan(planId: string): Promise<void> {
return request({
url: `/api/v1/health/diet/plans/${planId}/activate`,
method: "POST",
});
},
/**
* 获取推荐食谱
*
* @param params 查询参数
* @returns 推荐食谱列表
*/
getRecommendedRecipes(params: RecipeQueryParams): Promise<RecipeListResult> {
return request<RecipeListResult>({
url: "/api/v1/health/diet/recipes/recommend",
method: "GET",
data: params,
});
},
};
export default DietAPI;
/** 食物查询参数 */
export interface FoodQueryParams {
page: number;
pageSize: number;
}
/** 食物列表响应 */
export interface FoodListResult {
list: Food[];
total: number;
page: number;
pageSize: number;
}
/** 食物详情响应 */
export interface FoodDetailResult {
food: Food;
}
/** 食物信息 */
export interface Food {
id: string;
name: string;
category: string;
calories: number;
nutrients: Nutrients;
glycemicIndex?: number;
allergens?: string[];
}
/** 营养成分 */
export interface Nutrients {
protein: number;
carbohydrates: number;
fat: number;
fiber: number;
sodium: number;
vitamins: Record<string, number>;
minerals: Record<string, number>;
}
/** 用餐记录数据 */
export interface MealRecordData {
mealType: "breakfast" | "lunch" | "dinner" | "snack";
foods: FoodItemData[];
recordTime: string;
images?: string[];
}
/** 食物项目数据 */
export interface FoodItemData {
foodId: string;
foodName: string;
quantity: number;
}
/** 用餐记录 */
export interface MealRecord {
id: string;
userId: string;
mealType: "breakfast" | "lunch" | "dinner" | "snack";
foods: FoodItem[];
totalCalories: number;
totalNutrients: Nutrients;
recordTime: string;
images?: string[];
}
/** 食物项目 */
export interface FoodItem {
foodId: string;
foodName: string;
quantity: number;
calories: number;
nutrients: Nutrients;
}
/** 用餐记录响应 */
export interface MealRecordResult {
record: MealRecord;
}
/** 用餐记录查询参数 */
export interface MealRecordQueryParams {
date?: string;
mealType?: string;
page: number;
pageSize: number;
}
/** 用餐记录列表响应 */
export interface MealRecordListResult {
list: MealRecord[];
total: number;
page: number;
pageSize: number;
}
/** 营养分析参数 */
export interface NutritionAnalysisParams {
startDate: string;
endDate: string;
}
/** 营养分析响应 */
export interface NutritionAnalysisResult {
analysis: NutritionAnalysis;
}
/** 营养分析 */
export interface NutritionAnalysis {
totalCalories: number;
averageCalories: number;
totalNutrients: Nutrients;
dailyGoals: Nutrients;
achievements: Record<string, number>;
suggestions: string[];
}
/** 膳食计划数据 */
export interface DietPlanData {
name: string;
type: "weight_loss" | "weight_gain" | "maintenance" | "diabetic" | "hypertension";
targetCalories: number;
targetNutrients: Nutrients;
duration: number;
meals: PlannedMealData[];
}
/** 计划用餐数据 */
export interface PlannedMealData {
mealType: "breakfast" | "lunch" | "dinner" | "snack";
foods: FoodItemData[];
targetCalories: number;
instructions?: string;
}
/** 膳食计划 */
export interface DietPlan {
id: string;
userId: string;
name: string;
type: "weight_loss" | "weight_gain" | "maintenance" | "diabetic" | "hypertension";
targetCalories: number;
targetNutrients: Nutrients;
duration: number;
meals: PlannedMeal[];
createdDate: string;
isActive: boolean;
}
/** 计划用餐 */
export interface PlannedMeal {
mealType: "breakfast" | "lunch" | "dinner" | "snack";
foods: FoodItem[];
targetCalories: number;
instructions?: string;
}
/** 膳食计划列表响应 */
export interface DietPlanListResult {
list: DietPlan[];
}
/** 膳食计划响应 */
export interface DietPlanResult {
plan: DietPlan;
}
/** 食谱查询参数 */
export interface RecipeQueryParams {
mealType: string;
calories?: number;
dietType?: string;
}
/** 食谱列表响应 */
export interface RecipeListResult {
list: Recipe[];
}
/** 食谱信息 */
export interface Recipe {
id: string;
name: string;
description: string;
mealType: string;
calories: number;
nutrients: Nutrients;
ingredients: string[];
instructions: string[];
images: string[];
cookingTime: number;
difficulty: "easy" | "medium" | "hard";
}

359
src/api/health/education.ts Normal file
View File

@ -0,0 +1,359 @@
import request from "@/utils/request";
const EducationAPI = {
/**
* 获取文章列表
*
* @param params 查询参数
* @returns 文章列表
*/
getArticles(params: ArticleQueryParams): Promise<ArticleListResult> {
return request<ArticleListResult>({
url: "/api/v1/health/education/articles",
method: "GET",
data: params,
});
},
/**
* 获取文章详情
*
* @param articleId 文章ID
* @returns 文章详情
*/
getArticleDetail(articleId: string): Promise<ArticleDetailResult> {
return request<ArticleDetailResult>({
url: `/api/v1/health/education/articles/${articleId}`,
method: "GET",
});
},
/**
* 搜索文章
*
* @param keyword 关键词
* @param params 查询参数
* @returns 文章列表
*/
searchArticles(keyword: string, params: ArticleSearchParams): Promise<ArticleListResult> {
return request<ArticleListResult>({
url: "/api/v1/health/education/articles/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 点赞文章
*
* @param articleId 文章ID
*/
likeArticle(articleId: string): Promise<void> {
return request({
url: `/api/v1/health/education/articles/${articleId}/like`,
method: "POST",
});
},
/**
* 获取视频列表
*
* @param params 查询参数
* @returns 视频列表
*/
getVideos(params: VideoQueryParams): Promise<VideoListResult> {
return request<VideoListResult>({
url: "/api/v1/health/education/videos",
method: "GET",
data: params,
});
},
/**
* 获取视频详情
*
* @param videoId 视频ID
* @returns 视频详情
*/
getVideoDetail(videoId: string): Promise<VideoDetailResult> {
return request<VideoDetailResult>({
url: `/api/v1/health/education/videos/${videoId}`,
method: "GET",
});
},
/**
* 搜索视频
*
* @param keyword 关键词
* @param params 查询参数
* @returns 视频列表
*/
searchVideos(keyword: string, params: VideoSearchParams): Promise<VideoListResult> {
return request<VideoListResult>({
url: "/api/v1/health/education/videos/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 记录视频观看
*
* @param videoId 视频ID
* @param data 观看数据
*/
recordVideoView(videoId: string, data: VideoViewData): Promise<void> {
return request({
url: `/api/v1/health/education/videos/${videoId}/view`,
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取课程列表
*
* @param params 查询参数
* @returns 课程列表
*/
getCourses(params: CourseQueryParams): Promise<CourseListResult> {
return request<CourseListResult>({
url: "/api/v1/health/education/courses",
method: "GET",
data: params,
});
},
/**
* 获取课程详情
*
* @param courseId 课程ID
* @returns 课程详情
*/
getCourseDetail(courseId: string): Promise<CourseDetailResult> {
return request<CourseDetailResult>({
url: `/api/v1/health/education/courses/${courseId}`,
method: "GET",
});
},
/**
* 报名课程
*
* @param courseId 课程ID
*/
enrollCourse(courseId: string): Promise<void> {
return request({
url: `/api/v1/health/education/courses/${courseId}/enroll`,
method: "POST",
});
},
/**
* 获取学习进度
*
* @param courseId 课程ID
* @returns 学习进度
*/
getStudyProgress(courseId: string): Promise<StudyProgressResult> {
return request<StudyProgressResult>({
url: `/api/v1/health/education/courses/${courseId}/progress`,
method: "GET",
});
},
/**
* 完成课程
*
* @param courseId 课程ID
* @param lessonId 课时ID
*/
completeLesson(courseId: string, lessonId: string): Promise<void> {
return request({
url: `/api/v1/health/education/courses/${courseId}/lessons/${lessonId}/complete`,
method: "POST",
});
},
};
export default EducationAPI;
/** 文章查询参数 */
export interface ArticleQueryParams {
category?: string;
difficulty?: string;
page: number;
pageSize: number;
}
/** 文章搜索参数 */
export interface ArticleSearchParams {
page: number;
pageSize: number;
}
/** 文章列表响应 */
export interface ArticleListResult {
list: HealthArticle[];
total: number;
page: number;
pageSize: number;
}
/** 文章详情响应 */
export interface ArticleDetailResult {
article: HealthArticle;
}
/** 健康文章 */
export interface HealthArticle {
id: string;
title: string;
summary: string;
content: string;
category: string;
tags: string[];
author: string;
publishDate: string;
readCount: number;
likeCount: number;
difficulty: "beginner" | "intermediate" | "advanced";
images: string[];
relatedArticles: string[];
}
/** 视频查询参数 */
export interface VideoQueryParams {
category?: string;
difficulty?: string;
page: number;
pageSize: number;
}
/** 视频搜索参数 */
export interface VideoSearchParams {
page: number;
pageSize: number;
}
/** 视频列表响应 */
export interface VideoListResult {
list: HealthVideo[];
total: number;
page: number;
pageSize: number;
}
/** 视频详情响应 */
export interface VideoDetailResult {
video: HealthVideo;
}
/** 健康视频 */
export interface HealthVideo {
id: string;
title: string;
description: string;
category: string;
tags: string[];
instructor: string;
duration: number;
difficulty: "beginner" | "intermediate" | "advanced";
videoUrl: string;
thumbnailUrl: string;
publishDate: string;
viewCount: number;
likeCount: number;
relatedVideos: string[];
}
/** 视频观看数据 */
export interface VideoViewData {
watchDuration: number;
}
/** 课程查询参数 */
export interface CourseQueryParams {
category?: string;
difficulty?: string;
page: number;
pageSize: number;
}
/** 课程列表响应 */
export interface CourseListResult {
list: HealthCourse[];
total: number;
page: number;
pageSize: number;
}
/** 课程详情响应 */
export interface CourseDetailResult {
course: HealthCourse;
}
/** 健康课程 */
export interface HealthCourse {
id: string;
title: string;
description: string;
category: string;
instructor: string;
duration: number;
lessonCount: number;
difficulty: "beginner" | "intermediate" | "advanced";
price: number;
rating: number;
enrollmentCount: number;
lessons: CourseLesson[];
thumbnailUrl: string;
createdDate: string;
}
/** 课程课时 */
export interface CourseLesson {
id: string;
title: string;
description: string;
duration: number;
videoUrl?: string;
materials?: string[];
quiz?: Quiz;
order: number;
}
/** 测验 */
export interface Quiz {
id: string;
questions: QuizQuestion[];
passingScore: number;
}
/** 测验问题 */
export interface QuizQuestion {
id: string;
question: string;
options: string[];
correctAnswer: number;
explanation?: string;
}
/** 学习进度响应 */
export interface StudyProgressResult {
progress: StudyProgress;
}
/** 学习进度 */
export interface StudyProgress {
courseId: string;
completedLessons: string[];
totalLessons: number;
progressPercentage: number;
lastStudyDate: string;
totalStudyTime: number;
}

View File

@ -0,0 +1,253 @@
import request from "@/utils/request";
const EncyclopediaAPI = {
/**
* 获取分类列表
*
* @returns 分类列表
*/
getCategories(): Promise<CategoryListResult> {
return request<CategoryListResult>({
url: "/api/v1/health/encyclopedia/categories",
method: "GET",
});
},
/**
* 搜索中药
*
* @param keyword 关键词
* @param params 查询参数
* @returns 中药列表
*/
searchHerbs(keyword: string, params: HerbQueryParams): Promise<HerbListResult> {
return request<HerbListResult>({
url: "/api/v1/health/encyclopedia/herbs/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 获取中药详情
*
* @param herbId 中药ID
* @returns 中药详情
*/
getHerbDetail(herbId: string): Promise<HerbDetailResult> {
return request<HerbDetailResult>({
url: `/api/v1/health/encyclopedia/herbs/${herbId}`,
method: "GET",
});
},
/**
* 获取中药列表
*
* @param params 查询参数
* @returns 中药列表
*/
getHerbs(params: HerbFilterParams): Promise<HerbListResult> {
return request<HerbListResult>({
url: "/api/v1/health/encyclopedia/herbs",
method: "GET",
data: params,
});
},
/**
* 搜索穴位
*
* @param keyword 关键词
* @param params 查询参数
* @returns 穴位列表
*/
searchAcupoints(keyword: string, params: AcupointQueryParams): Promise<AcupointListResult> {
return request<AcupointListResult>({
url: "/api/v1/health/encyclopedia/acupoints/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 获取穴位详情
*
* @param acupointId 穴位ID
* @returns 穴位详情
*/
getAcupointDetail(acupointId: string): Promise<AcupointDetailResult> {
return request<AcupointDetailResult>({
url: `/api/v1/health/encyclopedia/acupoints/${acupointId}`,
method: "GET",
});
},
/**
* 获取穴位列表
*
* @param params 查询参数
* @returns 穴位列表
*/
getAcupoints(params: AcupointFilterParams): Promise<AcupointListResult> {
return request<AcupointListResult>({
url: "/api/v1/health/encyclopedia/acupoints",
method: "GET",
data: params,
});
},
/**
* 获取经络列表
*
* @returns 经络列表
*/
getMeridians(): Promise<MeridianListResult> {
return request<MeridianListResult>({
url: "/api/v1/health/encyclopedia/meridians",
method: "GET",
});
},
/**
* 获取经络详情
*
* @param meridianId 经络ID
* @returns 经络详情
*/
getMeridianDetail(meridianId: string): Promise<MeridianDetailResult> {
return request<MeridianDetailResult>({
url: `/api/v1/health/encyclopedia/meridians/${meridianId}`,
method: "GET",
});
},
};
export default EncyclopediaAPI;
/** 分类列表响应 */
export interface CategoryListResult {
list: EncyclopediaCategory[];
}
/** 百科分类 */
export interface EncyclopediaCategory {
id: string;
name: string;
type: "herb" | "acupoint" | "meridian" | "theory";
description: string;
icon: string;
itemCount: number;
}
/** 中药查询参数 */
export interface HerbQueryParams {
page: number;
pageSize: number;
}
/** 中药筛选参数 */
export interface HerbFilterParams {
category?: string;
nature?: string;
meridian?: string;
page: number;
pageSize: number;
}
/** 中药列表响应 */
export interface HerbListResult {
list: Herb[];
total: number;
page: number;
pageSize: number;
}
/** 中药详情响应 */
export interface HerbDetailResult {
herb: Herb;
}
/** 中药信息 */
export interface Herb {
id: string;
name: string;
latinName?: string;
aliases: string[];
category: string;
nature: "hot" | "warm" | "neutral" | "cool" | "cold";
taste: string[];
meridians: string[];
effects: string[];
indications: string[];
contraindications: string[];
dosage: string;
preparations: string[];
images: string[];
description: string;
}
/** 穴位查询参数 */
export interface AcupointQueryParams {
page: number;
pageSize: number;
}
/** 穴位筛选参数 */
export interface AcupointFilterParams {
meridian?: string;
category?: string;
page: number;
pageSize: number;
}
/** 穴位列表响应 */
export interface AcupointListResult {
list: Acupoint[];
total: number;
page: number;
pageSize: number;
}
/** 穴位详情响应 */
export interface AcupointDetailResult {
acupoint: Acupoint;
}
/** 穴位信息 */
export interface Acupoint {
id: string;
name: string;
location: string;
meridian: string;
category: string;
effects: string[];
indications: string[];
manipulations: string[];
precautions: string[];
images: string[];
videos?: string[];
}
/** 经络列表响应 */
export interface MeridianListResult {
list: Meridian[];
}
/** 经络详情响应 */
export interface MeridianDetailResult {
meridian: Meridian;
}
/** 经络信息 */
export interface Meridian {
id: string;
name: string;
type: "main" | "extraordinary";
pathways: string[];
acupoints: string[];
functions: string[];
associatedOrgans: string[];
flowTime: string;
images: string[];
}

343
src/api/health/exercise.ts Normal file
View File

@ -0,0 +1,343 @@
import request from "@/utils/request";
const ExerciseAPI = {
/**
* 获取运动列表
*
* @param params 查询参数
* @returns 运动列表
*/
getExercises(params: ExerciseQueryParams): Promise<ExerciseListResult> {
return request<ExerciseListResult>({
url: "/api/v1/health/exercise/exercises",
method: "GET",
data: params,
});
},
/**
* 获取运动详情
*
* @param exerciseId 运动ID
* @returns 运动详情
*/
getExerciseDetail(exerciseId: string): Promise<ExerciseDetailResult> {
return request<ExerciseDetailResult>({
url: `/api/v1/health/exercise/exercises/${exerciseId}`,
method: "GET",
});
},
/**
* 记录运动
*
* @param data 运动数据
* @returns 运动记录
*/
recordExercise(data: ExerciseRecordData): Promise<ExerciseRecordResult> {
return request<ExerciseRecordResult>({
url: "/api/v1/health/exercise/records",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取运动记录
*
* @param params 查询参数
* @returns 运动记录列表
*/
getExerciseRecords(params: ExerciseRecordQueryParams): Promise<ExerciseRecordListResult> {
return request<ExerciseRecordListResult>({
url: "/api/v1/health/exercise/records/history",
method: "GET",
data: params,
});
},
/**
* 获取运动统计
*
* @param params 查询参数
* @returns 运动统计数据
*/
getExerciseStats(params: ExerciseStatsParams): Promise<ExerciseStatsResult> {
return request<ExerciseStatsResult>({
url: "/api/v1/health/exercise/stats",
method: "GET",
data: params,
});
},
/**
* 获取运动计划
*
* @returns 运动计划列表
*/
getExercisePlans(): Promise<ExercisePlanListResult> {
return request<ExercisePlanListResult>({
url: "/api/v1/health/exercise/plans",
method: "GET",
});
},
/**
* 创建运动计划
*
* @param data 运动计划数据
* @returns 运动计划
*/
createExercisePlan(data: ExercisePlanData): Promise<ExercisePlanResult> {
return request<ExercisePlanResult>({
url: "/api/v1/health/exercise/plans",
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 激活运动计划
*
* @param planId 计划ID
*/
activateExercisePlan(planId: string): Promise<void> {
return request({
url: `/api/v1/health/exercise/plans/${planId}/activate`,
method: "POST",
});
},
/**
* 获取推荐运动
*
* @param params 查询参数
* @returns 推荐运动列表
*/
getRecommendedExercises(params: RecommendedExerciseParams): Promise<RecommendedExerciseResult> {
return request<RecommendedExerciseResult>({
url: "/api/v1/health/exercise/recommend",
method: "GET",
data: params,
});
},
};
export default ExerciseAPI;
/** 运动查询参数 */
export interface ExerciseQueryParams {
category?: string;
difficulty?: string;
duration?: number;
page: number;
pageSize: number;
}
/** 运动列表响应 */
export interface ExerciseListResult {
list: Exercise[];
total: number;
page: number;
pageSize: number;
}
/** 运动详情响应 */
export interface ExerciseDetailResult {
exercise: Exercise;
}
/** 运动信息 */
export interface Exercise {
id: string;
name: string;
category: "cardio" | "strength" | "flexibility" | "balance" | "sports";
difficulty: "beginner" | "intermediate" | "advanced";
duration: number;
caloriesPerMinute: number;
equipment?: string[];
instructions: string[];
images?: string[];
videos?: string[];
}
/** 运动记录数据 */
export interface ExerciseRecordData {
exerciseId: string;
exerciseName: string;
duration: number;
intensity: "low" | "medium" | "high";
heartRate?: {
average: number;
maximum: number;
};
distance?: number;
sets?: ExerciseSetData[];
recordTime: string;
notes?: string;
}
/** 运动组数据 */
export interface ExerciseSetData {
reps: number;
weight?: number;
restTime?: number;
}
/** 运动记录 */
export interface ExerciseRecord {
id: string;
userId: string;
exerciseId: string;
exerciseName: string;
duration: number;
intensity: "low" | "medium" | "high";
caloriesBurned: number;
heartRate?: {
average: number;
maximum: number;
};
distance?: number;
sets?: ExerciseSet[];
recordTime: string;
notes?: string;
}
/** 运动组 */
export interface ExerciseSet {
reps: number;
weight?: number;
restTime?: number;
}
/** 运动记录响应 */
export interface ExerciseRecordResult {
record: ExerciseRecord;
}
/** 运动记录查询参数 */
export interface ExerciseRecordQueryParams {
startDate?: string;
endDate?: string;
page: number;
pageSize: number;
}
/** 运动记录列表响应 */
export interface ExerciseRecordListResult {
list: ExerciseRecord[];
total: number;
page: number;
pageSize: number;
}
/** 运动统计参数 */
export interface ExerciseStatsParams {
period: "7d" | "30d" | "90d" | "1y";
}
/** 运动统计响应 */
export interface ExerciseStatsResult {
stats: ExerciseStats;
}
/** 运动统计 */
export interface ExerciseStats {
totalDuration: number;
totalCalories: number;
totalWorkouts: number;
averageDuration: number;
averageCalories: number;
categoryBreakdown: Record<string, number>;
weeklyTrend: Array<{
date: string;
duration: number;
calories: number;
}>;
}
/** 运动计划数据 */
export interface ExercisePlanData {
name: string;
goal: "weight_loss" | "muscle_gain" | "endurance" | "strength" | "general_fitness";
difficulty: "beginner" | "intermediate" | "advanced";
duration: number;
weeklySchedule: WeeklyScheduleData[];
}
/** 周计划数据 */
export interface WeeklyScheduleData {
dayOfWeek: number;
exercises: PlannedExerciseData[];
restDay: boolean;
}
/** 计划运动数据 */
export interface PlannedExerciseData {
exerciseId: string;
exerciseName: string;
duration?: number;
sets?: number;
reps?: number;
weight?: number;
restTime?: number;
}
/** 运动计划 */
export interface ExercisePlan {
id: string;
userId: string;
name: string;
goal: "weight_loss" | "muscle_gain" | "endurance" | "strength" | "general_fitness";
difficulty: "beginner" | "intermediate" | "advanced";
duration: number;
weeklySchedule: WeeklySchedule[];
createdDate: string;
isActive: boolean;
}
/** 周计划 */
export interface WeeklySchedule {
dayOfWeek: number;
exercises: PlannedExercise[];
restDay: boolean;
}
/** 计划运动 */
export interface PlannedExercise {
exerciseId: string;
exerciseName: string;
duration?: number;
sets?: number;
reps?: number;
weight?: number;
restTime?: number;
}
/** 运动计划列表响应 */
export interface ExercisePlanListResult {
list: ExercisePlan[];
}
/** 运动计划响应 */
export interface ExercisePlanResult {
plan: ExercisePlan;
}
/** 推荐运动参数 */
export interface RecommendedExerciseParams {
goal?: string;
duration?: number;
difficulty?: string;
}
/** 推荐运动响应 */
export interface RecommendedExerciseResult {
list: Exercise[];
}

359
src/api/health/profile.ts Normal file
View File

@ -0,0 +1,359 @@
import request from "@/utils/request";
const EducationAPI = {
/**
* 获取文章列表
*
* @param params 查询参数
* @returns 文章列表
*/
getArticles(params: ArticleQueryParams): Promise<ArticleListResult> {
return request<ArticleListResult>({
url: "/api/v1/health/education/articles",
method: "GET",
data: params,
});
},
/**
* 获取文章详情
*
* @param articleId 文章ID
* @returns 文章详情
*/
getArticleDetail(articleId: string): Promise<ArticleDetailResult> {
return request<ArticleDetailResult>({
url: `/api/v1/health/education/articles/${articleId}`,
method: "GET",
});
},
/**
* 搜索文章
*
* @param keyword 关键词
* @param params 查询参数
* @returns 文章列表
*/
searchArticles(keyword: string, params: ArticleSearchParams): Promise<ArticleListResult> {
return request<ArticleListResult>({
url: "/api/v1/health/education/articles/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 点赞文章
*
* @param articleId 文章ID
*/
likeArticle(articleId: string): Promise<void> {
return request({
url: `/api/v1/health/education/articles/${articleId}/like`,
method: "POST",
});
},
/**
* 获取视频列表
*
* @param params 查询参数
* @returns 视频列表
*/
getVideos(params: VideoQueryParams): Promise<VideoListResult> {
return request<VideoListResult>({
url: "/api/v1/health/education/videos",
method: "GET",
data: params,
});
},
/**
* 获取视频详情
*
* @param videoId 视频ID
* @returns 视频详情
*/
getVideoDetail(videoId: string): Promise<VideoDetailResult> {
return request<VideoDetailResult>({
url: `/api/v1/health/education/videos/${videoId}`,
method: "GET",
});
},
/**
* 搜索视频
*
* @param keyword 关键词
* @param params 查询参数
* @returns 视频列表
*/
searchVideos(keyword: string, params: VideoSearchParams): Promise<VideoListResult> {
return request<VideoListResult>({
url: "/api/v1/health/education/videos/search",
method: "GET",
data: { keyword, ...params },
});
},
/**
* 记录视频观看
*
* @param videoId 视频ID
* @param data 观看数据
*/
recordVideoView(videoId: string, data: VideoViewData): Promise<void> {
return request({
url: `/api/v1/health/education/videos/${videoId}/view`,
method: "POST",
data: data,
header: {
"Content-Type": "application/json",
},
});
},
/**
* 获取课程列表
*
* @param params 查询参数
* @returns 课程列表
*/
getCourses(params: CourseQueryParams): Promise<CourseListResult> {
return request<CourseListResult>({
url: "/api/v1/health/education/courses",
method: "GET",
data: params,
});
},
/**
* 获取课程详情
*
* @param courseId 课程ID
* @returns 课程详情
*/
getCourseDetail(courseId: string): Promise<CourseDetailResult> {
return request<CourseDetailResult>({
url: `/api/v1/health/education/courses/${courseId}`,
method: "GET",
});
},
/**
* 报名课程
*
* @param courseId 课程ID
*/
enrollCourse(courseId: string): Promise<void> {
return request({
url: `/api/v1/health/education/courses/${courseId}/enroll`,
method: "POST",
});
},
/**
* 获取学习进度
*
* @param courseId 课程ID
* @returns 学习进度
*/
getStudyProgress(courseId: string): Promise<StudyProgressResult> {
return request<StudyProgressResult>({
url: `/api/v1/health/education/courses/${courseId}/progress`,
method: "GET",
});
},
/**
* 完成课程
*
* @param courseId 课程ID
* @param lessonId 课时ID
*/
completeLesson(courseId: string, lessonId: string): Promise<void> {
return request({
url: `/api/v1/health/education/courses/${courseId}/lessons/${lessonId}/complete`,
method: "POST",
});
},
};
export default EducationAPI;
/** 文章查询参数 */
export interface ArticleQueryParams {
category?: string;
difficulty?: string;
page: number;
pageSize: number;
}
/** 文章搜索参数 */
export interface ArticleSearchParams {
page: number;
pageSize: number;
}
/** 文章列表响应 */
export interface ArticleListResult {
list: HealthArticle[];
total: number;
page: number;
pageSize: number;
}
/** 文章详情响应 */
export interface ArticleDetailResult {
article: HealthArticle;
}
/** 健康文章 */
export interface HealthArticle {
id: string;
title: string;
summary: string;
content: string;
category: string;
tags: string[];
author: string;
publishDate: string;
readCount: number;
likeCount: number;
difficulty: "beginner" | "intermediate" | "advanced";
images: string[];
relatedArticles: string[];
}
/** 视频查询参数 */
export interface VideoQueryParams {
category?: string;
difficulty?: string;
page: number;
pageSize: number;
}
/** 视频搜索参数 */
export interface VideoSearchParams {
page: number;
pageSize: number;
}
/** 视频列表响应 */
export interface VideoListResult {
list: HealthVideo[];
total: number;
page: number;
pageSize: number;
}
/** 视频详情响应 */
export interface VideoDetailResult {
video: HealthVideo;
}
/** 健康视频 */
export interface HealthVideo {
id: string;
title: string;
description: string;
category: string;
tags: string[];
instructor: string;
duration: number;
difficulty: "beginner" | "intermediate" | "advanced";
videoUrl: string;
thumbnailUrl: string;
publishDate: string;
viewCount: number;
likeCount: number;
relatedVideos: string[];
}
/** 视频观看数据 */
export interface VideoViewData {
watchDuration: number;
}
/** 课程查询参数 */
export interface CourseQueryParams {
category?: string;
difficulty?: string;
page: number;
pageSize: number;
}
/** 课程列表响应 */
export interface CourseListResult {
list: HealthCourse[];
total: number;
page: number;
pageSize: number;
}
/** 课程详情响应 */
export interface CourseDetailResult {
course: HealthCourse;
}
/** 健康课程 */
export interface HealthCourse {
id: string;
title: string;
description: string;
category: string;
instructor: string;
duration: number;
lessonCount: number;
difficulty: "beginner" | "intermediate" | "advanced";
price: number;
rating: number;
enrollmentCount: number;
lessons: CourseLesson[];
thumbnailUrl: string;
createdDate: string;
}
/** 课程课时 */
export interface CourseLesson {
id: string;
title: string;
description: string;
duration: number;
videoUrl?: string;
materials?: string[];
quiz?: Quiz;
order: number;
}
/** 测验 */
export interface Quiz {
id: string;
questions: QuizQuestion[];
passingScore: number;
}
/** 测验问题 */
export interface QuizQuestion {
id: string;
question: string;
options: string[];
correctAnswer: number;
explanation?: string;
}
/** 学习进度响应 */
export interface StudyProgressResult {
progress: StudyProgress;
}
/** 学习进度 */
export interface StudyProgress {
courseId: string;
completedLessons: string[];
totalLessons: number;
progressPercentage: number;
lastStudyDate: string;
totalStudyTime: number;
}

104
src/api/system/config.ts Normal file
View File

@ -0,0 +1,104 @@
import request from "@/utils/request";
const CONFIG_BASE_URL = "/api/v1/config";
const ConfigAPI = {
/** 获取系统配置分页数据 */
getPage(queryParams: ConfigPageQuery) {
return request<PageResult<ConfigPageVO[]>>({
url: `${CONFIG_BASE_URL}/page`,
method: "GET",
data: queryParams,
});
},
/**
* 获取系统配置表单数据
*
* @param id ConfigID
* @returns Config表单数据
*/
getFormData(id: number) {
return request<ConfigForm>({
url: `${CONFIG_BASE_URL}/${id}/form`,
method: "GET",
});
},
/** 添加系统配置*/
add(data: ConfigForm) {
return request({
url: `${CONFIG_BASE_URL}`,
method: "POST",
data: data,
});
},
/**
* 更新系统配置
*
* @param id ConfigID
* @param data Config表单数据
*/
update(id: number, data: ConfigForm) {
return request({
url: `${CONFIG_BASE_URL}/${id}`,
method: "PUT",
data: data,
});
},
/**
* 删除系统配置
*
* @param ids 系统配置ID
*/
deleteById(id: number) {
return request({
url: `${CONFIG_BASE_URL}/${id}`,
method: "DELETE",
});
},
refreshCache() {
return request({
url: `${CONFIG_BASE_URL}/refresh`,
method: "PUT",
});
},
};
export default ConfigAPI;
/** $系统配置分页查询参数 */
export interface ConfigPageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
}
/** 系统配置表单对象 */
export interface ConfigForm {
/** 主键 */
id?: number;
/** 配置名称 */
configName?: string;
/** 配置键 */
configKey?: string;
/** 配置值 */
configValue?: string;
/** 描述、备注 */
remark?: string;
}
/** 系统配置分页对象 */
export interface ConfigPageVO {
/** 主键 */
id?: number;
/** 配置名称 */
configName?: string;
/** 配置键 */
configKey?: string;
/** 配置值 */
configValue?: string;
/** 描述、备注 */
remark?: string;
}

130
src/api/system/dept.ts Normal file
View File

@ -0,0 +1,130 @@
import request from "@/utils/request";
const DEPT_BASE_URL = "/api/v1/dept";
const DeptAPI = {
/**
* 获取部门列表
*
* @param queryParams 查询参数(可选)
* @returns 部门树形表格数据
*/
getList(queryParams?: DeptQuery) {
return request<DeptVO[]>({
url: `${DEPT_BASE_URL}`,
method: "GET",
data: queryParams,
});
},
/** 获取部门下拉列表 */
getOptions() {
return request<OptionType[]>({
url: `${DEPT_BASE_URL}/options`,
method: "GET",
});
},
/**
* 获取部门表单数据
*
* @param id 部门ID
* @returns 部门表单数据
*/
getFormData(id: number) {
return request<DeptForm>({
url: `${DEPT_BASE_URL}/${id}/form`,
method: "GET",
});
},
/**
* 新增部门
*
* @param data 部门表单数据
* @returns 请求结果
*/
add(data: DeptForm) {
return request({
url: `${DEPT_BASE_URL}`,
method: "POST",
data: data,
});
},
/**
* 修改部门
*
* @param id 部门ID
* @param data 部门表单数据
* @returns 请求结果
*/
update(id: number, data: DeptForm) {
return request({
url: `${DEPT_BASE_URL}/${id}`,
method: "PUT",
data: data,
});
},
/**
* 删除部门
*
* @param ids 部门ID多个以英文逗号(,)分隔
* @returns 请求结果
*/
deleteByIds(ids: string) {
return request({
url: `${DEPT_BASE_URL}/${ids}`,
method: "DELETE",
});
},
};
export default DeptAPI;
/** 部门查询参数 */
export interface DeptQuery {
/** 搜索关键字 */
keywords?: string;
/** 状态 */
status?: number;
}
/** 部门类型 */
export interface DeptVO {
/** 子部门 */
children?: DeptVO[];
/** 创建时间 */
createTime?: Date;
/** 部门ID */
id?: number;
/** 部门名称 */
name?: string;
/** 部门编号 */
code?: string;
/** 父部门ID */
parentId?: number;
/** 排序 */
sort?: number;
/** 状态(1:启用0:禁用) */
status?: number;
/** 修改时间 */
updateTime?: Date;
}
/** 部门表单类型 */
export interface DeptForm {
/** 部门ID(新增不填) */
id?: number;
/** 部门名称 */
name?: string;
/** 部门编号 */
code?: string;
/** 父部门ID */
parentId: number;
/** 排序 */
sort?: number;
/** 状态(1:启用0禁用) */
status?: number;
}

305
src/api/system/dict.ts Normal file
View File

@ -0,0 +1,305 @@
import request from "@/utils/request";
const DICT_BASE_URL = "/api/v1/dicts";
const DictAPI = {
//---------------------------------------------------
// 字典相关接口
//---------------------------------------------------
/**
* 字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
getPage(queryParams: DictPageQuery) {
return request<PageResult<DictPageVO[]>>({
url: `${DICT_BASE_URL}/page`,
method: "GET",
data: queryParams,
});
},
/**
* 字典表单数据
*
* @param id 字典ID
* @returns 字典表单数据
*/
getFormData(id: number) {
return request<DictForm>({
url: `${DICT_BASE_URL}/${id}/form`,
method: "GET",
});
},
/**
* 新增字典
*
* @param data 字典表单数据
*/
create(data: DictForm) {
return request({
url: `${DICT_BASE_URL}`,
method: "POST",
data: data,
});
},
/**
* 修改字典
*
* @param id 字典ID
* @param data 字典表单数据
*/
update(id: number, data: DictForm) {
return request({
url: `${DICT_BASE_URL}/${id}`,
method: "PUT",
data: data,
});
},
/**
* 删除字典
*
* @param ids 字典ID多个以英文逗号(,)分隔
*/
deleteByIds(ids: string) {
return request({
url: `${DICT_BASE_URL}/${ids}`,
method: "DELETE",
});
},
//---------------------------------------------------
// 字典项相关接口
//---------------------------------------------------
/**
* 获取字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
return request<PageResult<DictItemPageVO[]>>({
url: `${DICT_BASE_URL}/${dictCode}/items/page`,
method: "GET",
data: queryParams,
});
},
/**
* 获取字典项列表
*/
getDictItems(dictCode: string) {
return request<DictItemOption[]>({
url: `${DICT_BASE_URL}/${dictCode}/items`,
method: "GET",
});
},
/**
* 新增字典项
*/
createDictItem(dictCode: string, data: DictItemForm) {
return request({
url: `${DICT_BASE_URL}/${dictCode}/items`,
method: "POST",
data: data,
});
},
/**
* 获取字典项表单数据
*
* @param id 字典项ID
* @returns 字典项表单数据
*/
getDictItemFormData(dictCode: string, id: number) {
return request<DictItemForm>({
url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`,
method: "GET",
});
},
/**
* 修改字典项
*/
updateDictItem(dictCode: string, id: number, data: DictItemForm) {
return request({
url: `${DICT_BASE_URL}/${dictCode}/items/${id}`,
method: "PUT",
data: data,
});
},
/**
* 删除字典项
*/
deleteDictItems(dictCode: string, ids: string) {
return request({
url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`,
method: "DELETE",
});
},
};
export default DictAPI;
/**
* 字典查询参数
*/
export interface DictPageQuery extends PageQuery {
/**
* 关键字(字典名称/编码)
*/
keywords?: string;
/**
* 字典状态1:启用0:禁用)
*/
status?: number;
}
/**
* 字典分页对象
*/
export interface DictPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典名称
*/
name: string;
/**
* 字典编码
*/
dictCode: string;
/**
* 字典状态1:启用0:禁用)
*/
status: number;
}
/**
* 字典
*/
export interface DictForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典名称
*/
name?: string;
/**
* 字典编码
*/
dictCode?: string;
/**
* 字典状态1-启用0-禁用)
*/
status?: number;
/**
* 备注
*/
remark?: string;
}
/**
* 字典查询参数
*/
export interface DictItemPageQuery extends PageQuery {
/** 关键字(字典数据值/标签) */
keywords?: string;
/** 字典编码 */
dictCode?: string;
}
/**
* 字典分页对象
*/
export interface DictItemPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典编码
*/
dictCode: string;
/**
* 字典数据值
*/
value: string;
/**
* 字典数据标签
*/
label: string;
/**
* 状态1:启用0:禁用)
*/
status: number;
/**
* 字典排序
*/
sort?: number;
}
/**
* 字典
*/
export interface DictItemForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典编码
*/
dictCode?: string;
/**
* 字典数据值
*/
value?: string;
/**
* 字典数据标签
*/
label?: string;
/**
* 状态1:启用0:禁用)
*/
status?: number;
/**
* 字典排序
*/
sort?: number;
/**
* 标签类型
*/
tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined;
}
/**
* 字典项下拉选项
*/
export interface DictItemOption {
/** 字典数据值 */
value: string | number;
/** 字典数据标签 */
label: string;
/** 标签类型 */
tagType?: "" | "success" | "info" | "warning" | "danger" | "primary";
/** 允许其他属性 */
[key: string]: any;
}

123
src/api/system/log.ts Normal file
View File

@ -0,0 +1,123 @@
import request from "@/utils/request";
const LOG_BASE_URL = "/api/v1/logs";
const LogAPI = {
/**
* 获取日志分页列表
*
* @param queryParams 查询参数
*/
getPage(queryParams: LogPageQuery) {
return request<PageResult<LogVO[]>>({
url: `${LOG_BASE_URL}/page`,
method: "GET",
data: queryParams,
});
},
/**
* 获取访问趋势
*
* @param queryParams
* @returns
*/
getVisitTrend(queryParams: VisitTrendQuery) {
return request<VisitTrendVO>({
url: `${LOG_BASE_URL}/visit-trend`,
method: "GET",
data: queryParams,
});
},
/**
* 获取访问趋势
*
* @param queryParams
* @returns
*/
getVisitStats() {
return request<VisitStatsVO>({
url: `${LOG_BASE_URL}/visit-stats`,
method: "GET",
});
},
};
export default LogAPI;
/**
* 日志分页查询对象
*/
export interface LogPageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
/** 操作时间 */
createTime?: [string, string] | string;
}
/**
* 系统日志分页VO
*/
export interface LogVO {
/** 主键 */
id?: number;
/** 日志模块 */
module?: string;
/** 日志内容 */
content?: string;
/** 请求路径 */
requestUri?: string;
/** 请求方法 */
method?: string;
/** IP 地址 */
ip?: string;
/** 地区 */
region?: string;
/** 浏览器 */
browser?: string;
/** 终端系统 */
os?: string;
/** 执行时间(毫秒) */
executionTime?: number;
/** 操作人 */
operator?: string;
/** 操作时间 */
createTime?: string;
}
/** 访问趋势视图对象 */
export interface VisitTrendVO {
/** 日期列表 */
dates: string[];
/** 浏览量(PV) */
pvList: number[];
/** 访客数(UV) */
uvList: number[];
/** IP数 */
ipList: number[];
}
/** 访问趋势查询参数 */
export interface VisitTrendQuery {
/** 开始日期 */
startDate: string;
/** 结束日期 */
endDate: string;
}
/** 访问统计 */
export interface VisitStatsVO {
/** 今日访客数(UV) */
todayUvCount: number;
/** 总访客数 */
totalUvCount: number;
/** 访客数同比增长率(相对于昨天同一时间段的增长率) */
uvGrowthRate: number;
/** 今日浏览量(PV) */
todayPvCount: number;
/** 总浏览量 */
totalPvCount: number;
/** 同比增长率(相对于昨天同一时间段的增长率) */
pvGrowthRate: number;
}

76
src/api/system/menu.ts Normal file
View File

@ -0,0 +1,76 @@
import request from "@/utils/request";
// 菜单基础URL
const MENU_BASE_URL = "/api/v1/menus";
const MenuAPI = {
/**
* 获取当前用户的路由列表
* <p/>
* 无需传入角色后端解析token获取角色自行判断是否拥有路由的权限
*
* @returns 路由列表
*/
getRoutes() {
return request<RouteVO[]>({
url: `${MENU_BASE_URL}/routes`,
method: "GET",
});
},
/**
* 获取菜单下拉数据源
*
* @returns 菜单下拉数据源
*/
getOptions(onlyParent?: boolean) {
return request<OptionType[]>({
url: `${MENU_BASE_URL}/options`,
method: "GET",
data: { onlyParent: onlyParent },
});
},
};
export default MenuAPI;
/** RouteVO路由对象 */
export interface RouteVO {
/** 子路由列表 */
children: RouteVO[];
/** 组件路径 */
component?: string;
/** 路由属性 */
meta?: Meta;
/** 路由名称 */
name?: string;
/** 路由路径 */
path?: string;
/** 跳转链接 */
redirect?: string;
}
/** Meta路由属性 */
export interface Meta {
/** 【目录】只有一个子路由是否始终显示 */
alwaysShow?: boolean;
/** 是否隐藏(true-是 false-否) */
hidden?: boolean;
/** ICON */
icon?: string;
/** 【菜单】是否开启页面缓存 */
keepAlive?: boolean;
/** 路由title */
title?: string;
}
/**
* 组件数据源
*/
interface OptionType {
/** 值 */
value: string | number;
/** 文本 */
label: string;
/** 子列表 */
children?: OptionType[];
}

201
src/api/system/notice.ts Normal file
View File

@ -0,0 +1,201 @@
import request from "@/utils/request";
const NOTICE_BASE_URL = "/api/v1/notices";
const NoticeAPI = {
/** 获取通知公告分页数据 */
getPage(queryParams?: NoticePageQuery) {
return request<PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/page`,
method: "GET",
data: queryParams,
});
},
/**
* 获取通知公告表单数据
*
* @param id NoticeID
* @returns Notice表单数据
*/
getFormData(id: number) {
return request<NoticeForm>({
url: `${NOTICE_BASE_URL}/${id}/form`,
method: "GET",
});
},
/**
* 添加通知公告
*
* @param data Notice表单数据
* @returns
*/
add(data: NoticeForm) {
return request({
url: `${NOTICE_BASE_URL}`,
method: "POST",
data: data,
});
},
/**
* 更新通知公告
*
* @param id NoticeID
* @param data Notice表单数据
*/
update(id: number, data: NoticeForm) {
return request({
url: `${NOTICE_BASE_URL}/${id}`,
method: "PUT",
data: data,
});
},
/**
* 批量删除通知公告,多个以英文逗号(,)分割
*
* @param ids 通知公告ID字符串多个以英文逗号(,)分割
*/
deleteByIds(ids: string) {
return request({
url: `${NOTICE_BASE_URL}/${ids}`,
method: "DELETE",
});
},
/**
* 发布通知
*
* @param id 被发布的通知公告id
* @returns
*/
publish(id: number) {
return request({
url: `${NOTICE_BASE_URL}/${id}/publish`,
method: "PUT",
});
},
/**
* 撤回通知
*
* @param id 撤回的通知id
* @returns
*/
revoke(id: number) {
return request({
url: `${NOTICE_BASE_URL}/${id}/revoke`,
method: "PUT",
});
},
/**
* 查看通知
*
* @param id
*/
getDetail(id: string) {
return request<NoticeDetailVO>({
url: `${NOTICE_BASE_URL}/${id}/detail`,
method: "GET",
});
},
/* 全部已读 */
readAll() {
return request({
url: `${NOTICE_BASE_URL}/read-all`,
method: "PUT",
});
},
/** 获取我的通知分页列表 */
getMyNoticePage(queryParams?: NoticePageQuery) {
return request<PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/my-page`,
method: "GET",
data: queryParams,
});
},
};
export default NoticeAPI;
/** 通知公告分页查询参数 */
export interface NoticePageQuery extends PageQuery {
/** 标题 */
title?: string;
/** 发布状态(0未发布1已发布-1已撤回) */
publishStatus?: number;
isRead?: number;
}
/** 通知公告表单对象 */
export interface NoticeForm {
id?: number;
/** 通知标题 */
title?: string;
/** 通知内容 */
content?: string;
/** 通知类型 */
type?: number;
/** 优先级(LMH高) */
level?: string;
/** 目标类型(1-全体 2-指定) */
targetType?: number;
/** 目标ID合集以,分割 */
targetUserIds?: string;
}
/** 通知公告分页对象 */
export interface NoticePageVO {
id: string;
/** 通知标题 */
title?: string;
/** 通知内容 */
content?: string;
/** 通知类型 */
type?: number;
/** 发布人 */
publisherName?: string;
/** 优先级(0-低 1-中 2-高) */
priority?: number;
/** 目标类型(0-全体 1-指定) */
targetType?: number;
/** 发布状态(0-未发布 1已发布 2已撤回) */
publishStatus?: number;
/** 发布时间 */
publishTime?: Date;
/** 撤回时间 */
revokeTime?: Date;
/** 优先级(L-低 M-中 H-高) */
level?: string | number;
}
export interface NoticeDetailVO {
/** 通知ID */
id?: string;
/** 通知标题 */
title?: string;
/** 通知内容 */
content?: string;
/** 通知类型 */
type?: number;
/** 发布人 */
publisherName?: string;
/** 优先级(L-低 M-中 H-高) */
level?: string;
/** 发布时间 */
publishTime?: Date;
/** 发布状态 */
publishStatus?: number;
}

138
src/api/system/role.ts Normal file
View File

@ -0,0 +1,138 @@
import request from "@/utils/request";
const ROLE_BASE_URL = "/api/v1/roles";
const RoleAPI = {
/** 获取角色分页数据 */
getPage(queryParams?: RolePageQuery) {
return request<PageResult<RolePageVO[]>>({
url: `${ROLE_BASE_URL}/page`,
method: "GET",
data: queryParams,
});
},
/** 获取角色下拉数据源 */
getOptions() {
return request<OptionType[]>({
url: `${ROLE_BASE_URL}/options`,
method: "GET",
});
},
/**
* 获取角色的菜单ID集合
*
* @param roleId 角色ID
* @returns 角色的菜单ID集合
*/
getRoleMenuIds(roleId: number) {
return request<number[]>({
url: `${ROLE_BASE_URL}/${roleId}/menuIds`,
method: "GET",
});
},
/**
* 分配菜单权限
*
* @param roleId 角色ID
* @param data 菜单ID集合
*/
updateRoleMenus(roleId: number, data: number[]) {
return request({
url: `${ROLE_BASE_URL}/${roleId}/menus`,
method: "PUT",
data: data,
});
},
/**
* 获取角色表单数据
*
* @param id 角色ID
* @returns 角色表单数据
*/
getFormData(id: number) {
return request<RoleForm>({
url: `${ROLE_BASE_URL}/${id}/form`,
method: "GET",
});
},
/** 添加角色 */
add(data: RoleForm) {
return request({
url: `${ROLE_BASE_URL}`,
method: "POST",
data: data,
});
},
/**
* 更新角色
*
* @param id 角色ID
* @param data 角色表单数据
*/
update(id: number, data: RoleForm) {
return request({
url: `${ROLE_BASE_URL}/${id}`,
method: "PUT",
data: data,
});
},
/**
* 批量删除角色,多个以英文逗号(,)分割
*
* @param ids 角色ID字符串多个以英文逗号(,)分割
*/
deleteByIds(ids: string) {
return request({
url: `${ROLE_BASE_URL}/${ids}`,
method: "DELETE",
});
},
};
export default RoleAPI;
/** 角色分页查询参数 */
export interface RolePageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
}
/** 角色分页对象 */
export interface RolePageVO {
/** 角色编码 */
code?: string;
/** 角色ID */
id: number;
/** 角色名称 */
name?: string;
/** 排序 */
sort?: number;
/** 角色状态 */
status?: number;
/** 创建时间 */
createTime?: string;
/** 修改时间 */
updateTime?: string;
}
/** 角色表单对象 */
export interface RoleForm {
/** 角色ID */
id?: number;
/** 角色编码 */
code?: string;
/** 数据权限 */
dataScope: number;
/** 角色名称 */
name?: string;
/** 排序 */
sort: number;
/** 角色状态(1-正常0-停用) */
status?: number;
}

316
src/api/system/user.ts Normal file
View File

@ -0,0 +1,316 @@
import request from "@/utils/request";
const USER_BASE_URL = "/api/v1/users";
const UserAPI = {
/**
* 获取当前登录用户信息
*
* @returns 登录用户昵称、头像信息,包括角色和权限
*/
getUserInfo(): Promise<UserInfo> {
return request<UserInfo>({
url: `${USER_BASE_URL}/me`,
method: "GET",
});
},
/**
* 获取用户分页列表
*
* @param queryParams 查询参数
*/
getPage(queryParams: UserPageQuery) {
return request<PageResult<UserPageVO[]>>({
url: `${USER_BASE_URL}/page`,
method: "GET",
data: queryParams,
});
},
/**
* 添加用户
*
* @param data 用户表单数据
*/
add(data: UserForm) {
return request({
url: `${USER_BASE_URL}`,
method: "POST",
data: data,
});
},
/**
* 获取用户表单详情
*
* @param userId 用户ID
* @returns 用户表单详情
*/
getFormData(userId: number) {
return request<UserForm>({
url: `${USER_BASE_URL}/${userId}/form`,
method: "GET",
});
},
/**
* 修改用户
*
* @param id 用户ID
* @param data 用户表单数据
*/
update(id: number, data: UserForm) {
return request({
url: `${USER_BASE_URL}/${id}`,
method: "PUT",
data: data,
});
},
/** 获取个人中心用户信息 */
getProfile() {
return request<UserProfileVO>({
url: `${USER_BASE_URL}/profile`,
method: "GET",
});
},
/** 修改个人中心用户信息 */
updateProfile(data: UserProfileForm) {
return request({
url: `${USER_BASE_URL}/profile`,
method: "PUT",
data: data,
});
},
/** 修改个人中心用户密码 */
changePassword(data: PasswordChangeForm) {
return request({
url: `${USER_BASE_URL}/password`,
method: "PUT",
data: data,
});
},
/**
* 发送手机/邮箱验证码
*
* @param contact 联系方式 手机号/邮箱
* @param contactType 联系方式类型 MOBILE:手机;EMAIL:邮箱
*/
sendVerificationCode(contact: string, contactType: string) {
return request({
url: `${USER_BASE_URL}/send-verification-code?contact=${contact}&contactType=${contactType}`,
method: "POST",
});
},
/** 绑定个人中心用户手机 */
bindMobile(data: MobileBindingForm) {
return request({
url: `${USER_BASE_URL}/mobile`,
method: "PUT",
data: data,
});
},
/** 绑定个人中心用户邮箱 */
bindEmail(data: EmailBindingForm) {
return request({
url: `${USER_BASE_URL}/email`,
method: "PUT",
data: data,
});
},
/**
* 批量删除用户,多个以英文逗号(,)分割
*
* @param ids 用户ID字符串多个以英文逗号(,)分割
*/
deleteByIds(ids: string) {
return request({
url: `${USER_BASE_URL}/${ids}`,
method: "DELETE",
});
},
};
export default UserAPI;
/** 登录用户信息 */
export interface UserInfo {
/** 用户ID */
userId?: number;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 头像URL */
avatar?: string;
/** 角色 */
roles?: string[];
/** 权限 */
perms?: string[];
}
/**
* 用户分页查询对象
*/
export interface UserPageQuery extends PageQuery {
/** 搜索关键字 */
keywords?: string;
/** 用户状态 */
status?: number;
/** 部门ID */
deptId?: number;
/** 开始时间 */
createTime?: [string, string] | string;
/** 排序字段 */
field?: string;
/** 排序方式(asc:正序,desc:倒序) */
direction?: string;
}
/** 用户分页对象 */
export interface UserPageVO {
/** 用户头像URL */
avatar?: string;
/** 创建时间 */
createTime?: string;
/** 部门名称 */
deptName?: string;
/** 用户邮箱 */
email?: string;
/** 性别 */
gender?: number;
/** 用户ID */
id: number;
/** 手机号 */
mobile?: string;
/** 用户昵称 */
nickname?: string;
/** 角色名称,多个使用英文逗号(,)分割 */
roleNames?: string;
/** 用户状态(1:启用;0:禁用) */
status?: number;
/** 用户名 */
username?: string;
}
/** 个人中心用户信息 */
export interface UserProfileVO {
/** 用户ID */
id?: number;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 头像URL */
avatar?: string;
/** 性别 */
gender?: number;
/** 手机号 */
mobile?: string;
/** 邮箱 */
email?: string;
/** 部门名称 */
deptName?: string;
/** 角色名称,多个使用英文逗号(,)分割 */
roleNames?: string;
/** 创建时间 */
createTime?: string;
}
/** 个人中心用户信息表单 */
export interface UserProfileForm {
/** 用户ID */
id?: number;
/** 用户名 */
username?: string;
/** 昵称 */
nickname?: string;
/** 头像URL */
avatar?: string;
/** 性别 */
gender?: number;
/** 手机号 */
mobile?: string;
/** 邮箱 */
email?: string;
}
/** 修改密码表单 */
export interface PasswordChangeForm {
/** 原密码 */
oldPassword?: string;
/** 新密码 */
newPassword?: string;
/** 确认新密码 */
confirmPassword?: string;
}
/** 修改手机表单 */
export interface MobileBindingForm {
/** 手机号 */
mobile?: string;
/** 验证码 */
code?: string;
}
/** 修改邮箱表单 */
export interface EmailBindingForm {
/** 邮箱 */
email?: string;
/** 验证码 */
code?: string;
}
/** 用户表单 */
export interface UserForm {
/** 用户头像 */
avatar?: string;
/** 部门ID */
deptId?: number;
/** 邮箱 */
email?: string;
/** 性别 */
gender?: number;
/** 用户ID */
id?: number;
/** 手机号 */
mobile?: string;
/** 昵称 */
nickname?: string;
/** 角色ID集合 */
roleIds: number[];
/** 用户状态(1:正常;0:禁用) */
status?: number;
/** 用户名 */
username?: string;
}

View File

@ -0,0 +1,82 @@
<template>
<wd-calendar
v-model="dateRange"
:label="label"
type="daterange"
:placeholder="placeholder"
@confirm="handleConfirm"
/>
</template>
<script lang="ts" setup>
import { dayjs } from "wot-design-uni";
const props = defineProps({
modelValue: {
type: [Array, String] as PropType<[string, string] | string | undefined>,
default: () => undefined,
},
placeholder: {
type: String,
default: "请选择时间范围",
},
label: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue"]);
const dateRange = ref<number[] | number | null>(null);
watch(
() => props.modelValue,
(val) => {
if (Array.isArray(val) && val.length === 2 && val[0] && val[1]) {
dateRange.value = val.map((item) => new Date(item).getTime());
} else if (typeof val === "string" && val.includes(",")) {
const [startDate, endDate] = val.split(",");
if (
startDate &&
endDate &&
!isNaN(new Date(startDate).getTime()) &&
!isNaN(new Date(endDate).getTime())
) {
dateRange.value = [new Date(startDate).getTime(), new Date(endDate).getTime()];
} else {
dateRange.value = null;
}
} else {
dateRange.value = null;
}
},
{
immediate: true,
}
);
// 确认选择时间
const handleConfirm = () => {
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
const startDate = dayjs(dateRange.value[0]).format("YYYY-MM-DD");
const endDate = dayjs(dateRange.value[1]).format("YYYY-MM-DD");
let newVal: any = [startDate, endDate];
// #ifdef MP-WEIXIN
newVal = `${startDate},${endDate}`;
// #endif
console.log("newVal", newVal);
emit("update:modelValue", newVal);
}
};
</script>
<style scoped>
.time-filter {
padding: 16rpx;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<template v-if="tagType">
<wd-tag :type="tagType" :round="round">{{ label }}</wd-tag>
</template>
<template v-else>
<view>{{ label }}</view>
</template>
</template>
<script setup lang="ts">
import { useDictStore } from "@/store/modules/dict";
const dictStore = useDictStore();
const props = defineProps({
code: String,
modelValue: [String, Number],
round: {
type: Boolean,
default: false,
},
});
const label = ref("");
const tagType = ref<string | undefined>();
const getLabelAndTagByValue = async (dictCode: string, value: any) => {
// 按需加载字典数据
await dictStore.loadDictItems(dictCode);
// 从缓存中获取字典数据
const dictItems = dictStore.getDictItems(dictCode);
// 查找对应的字典项
const dictItem = dictItems.find((item) => item.value == value);
return {
label: dictItem?.label || "",
tagType: dictItem?.tagType,
};
};
// 监听字典数据变化确保WebSocket更新时刷新标签
watch(
() => props.code && dictStore.getDictItems(props.code),
async () => {
if (props.code) {
await fetchLabelAndTag();
}
},
{ deep: true }
);
// 监听 props 的变化,获取并更新 label 和 tag
const fetchLabelAndTag = async () => {
if (!props.code || props.modelValue === undefined) return;
const result = await getLabelAndTagByValue(props.code, props.modelValue);
label.value = result.label;
tagType.value = result.tagType;
};
// 首次挂载时获取字典数据
onMounted(fetchLabelAndTag);
// 当 modelValue 发生变化时重新获取
watch(() => props.modelValue, fetchLabelAndTag);
</script>

View File

@ -0,0 +1,141 @@
<template>
<wd-picker
v-if="type === 'select' || type === 'radio'"
v-model="selectedValue"
:columns="options"
:label="label"
:placeholder="placeholder"
clearable
:rules="rules"
@confirm="handleChange"
/>
<wd-select-picker
v-else-if="type === 'checkbox'"
v-model="selectedValue"
:columns="options"
:placeholder="placeholder"
clearable
:label="label"
:rules="rules"
@confirm="handleChange"
/>
<wd-input
v-else
v-model="selectedValue"
:label="label"
clearable
:placeholder="placeholder"
:rules="rules"
@input="handleChange"
/>
</template>
<script setup lang="ts">
import { useDictStore } from "@/store/modules/dict";
const dictStore = useDictStore();
const props = defineProps({
code: {
type: String,
required: true,
},
modelValue: {
type: [String, Number, Array],
required: false,
},
label: {
type: String,
default: "",
},
type: {
type: String,
default: "select",
validator: (value: string) => ["select", "radio", "checkbox"].includes(value),
},
placeholder: {
type: String,
default: "请选择",
},
disabled: {
type: Boolean,
default: false,
},
rules: {
type: Array as PropType<any[]>,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue"]);
const options = ref<Array<{ label: string; value: string | number }>>([]);
const selectedValue = ref<any>(
typeof props.modelValue === "string" || typeof props.modelValue === "number"
? props.modelValue
: Array.isArray(props.modelValue)
? props.modelValue
: undefined
);
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newValue) => {
if (props.type === "checkbox") {
selectedValue.value = Array.isArray(newValue) ? newValue : [];
} else {
selectedValue.value = newValue?.toString() || "";
}
},
{ immediate: true }
);
// 监听 options 变化并重新匹配 selectedValue
watch(
() => options.value,
(newOptions) => {
// options 加载后,确保 selectedValue 可以正确匹配到 options
if (newOptions.length > 0 && selectedValue.value !== undefined) {
const matchedOption = newOptions.find((option) => option.value === selectedValue.value);
if (!matchedOption && props.type !== "checkbox") {
// 如果找不到匹配项,清空选中
selectedValue.value = "";
}
}
}
);
// 监听 selectedValue 的变化并触发 update:modelValue
function handleChange(val: any) {
emit("update:modelValue", val.value);
}
// 获取字典数据
onMounted(async () => {
if (!props.code) {
return;
}
// 按需加载字典数据
await dictStore.loadDictItems(props.code);
options.value = dictStore.getDictItems(props.code).map((item) => ({
label: item.label,
value: item.value,
}));
});
// 监听字典数据变化确保WebSocket更新时刷新选项
watch(
() => dictStore.getDictItems(props.code),
(newItems) => {
if (newItems.length > 0) {
options.value = newItems.map((item) => ({
label: item.label,
value: item.value,
}));
}
},
{ deep: true }
);
</script>

View File

@ -0,0 +1,174 @@
<template>
<wd-picker
v-model="selectedValues"
:columns="pickerColumns"
:display-format="displayFormat"
:column-change="handleColumnChange"
:label="label"
:placeholder="pickerColumns.length ? '请选择' : '加载中...'"
:required="required"
/>
</template>
<script setup lang="ts">
import { PickerViewInstance } from "wot-design-uni/components/wd-picker-view/types";
const props = defineProps({
modelValue: {
type: [Number, String],
},
data: {
required: true,
type: Array as () => OptionType[],
},
label: {
type: String,
default: "",
},
required: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(["update:modelValue"]);
// 定义响应式变量
const selectedValues = ref<number[] | string[]>([]);
const pickerColumns = ref<Array<Array<{ label: string; value: string | number }>>>([]);
// 监听 modelValue 的变化,更新 selectedValues
watch(
() => props.modelValue,
(val) => {
selectedValues.value = val ? findTreePath(val) : [];
}
);
/**
* 根据节点值查找路径
* 示例数据: [{"value":"1","label":"公司","children":[{"value":"2","label":"研发部"}]}]
* 查找部门ID为2的路径返回结果[1, 2]
*/
const findTreePath = (value: number | string): number[] | string[] => {
const numberPath: number[] = [];
const stringPath: string[] = [];
const list = props.data;
const find = (value: number | string, list: OptionType[]): boolean => {
for (const item of list) {
if (item.value === value) {
typeof value === "number" ? numberPath.push(value) : stringPath.push(value);
return true;
}
if (item.children?.length) {
typeof item.value === "number" ? numberPath.push(item.value) : stringPath.push(item.value);
if (find(value, item.children)) return true;
typeof item.value === "number" ? numberPath.pop() : stringPath.pop();
}
}
return false;
};
find(value, list);
return typeof value === "number" ? numberPath : stringPath;
};
/**
* 将树形数据转换为 Picker 所需的 columns 格式
*
* @param treeData 树形数据
* @returns Picker 所需的 columns 格式
*/
const transformTreeToColumns = (
treeData: OptionType[]
): Array<Array<{ label: string; value: string | number }>> => {
const columns: Array<Array<{ label: string; value: string | number }>> = [];
for (let depth = 0; depth <= selectedValues.value.length; depth++) {
const currentColumn = treeData.map((node) => ({ label: node.label, value: node.value }));
if (!currentColumn.length) break;
const selectedId = selectedValues.value[depth];
if (!currentColumn.some((item) => item.value === selectedId)) {
selectedValues.value[depth] = currentColumn[0]?.value;
}
columns.push(currentColumn);
const selectedNode = treeData.find((node) => node.value == selectedValues.value[depth]);
treeData = selectedNode?.children || [];
}
return columns;
};
// 监听 data 的变化,更新 pickerColumns
watch(
() => props.data,
(val) => {
console.log("监听 data 的变化", val);
pickerColumns.value = transformTreeToColumns(val);
},
{
immediate: true,
}
);
/**
* 处理列的变化,动态更新后续列的数据
*/
function handleColumnChange(
pickerView: PickerViewInstance,
value: Record<string, any> | Record<string, any>[],
columnIndex: number,
resolve: () => void
) {
const selectedValue = selectedValues.value[selectedValues.value.length - 1] || undefined;
emits("update:modelValue", selectedValue);
const item = Array.isArray(value) ? value[columnIndex] : value.value;
updatePickerColumns(pickerView, item.value, columnIndex);
resolve();
}
/**
* 动态更新所有后续列的数据
*/
function updatePickerColumns(
pickerView: PickerViewInstance,
parentId: string | number,
columnIndex: number
) {
const nextColumnIndex = columnIndex + 1;
const children = findChildren(parentId, props.data);
if (children.length > 0 && nextColumnIndex < 3) {
pickerView.setColumnData(nextColumnIndex, children);
updatePickerColumns(pickerView, children[0].value, nextColumnIndex);
}
}
/**
* 根据节点value查找其子节点数据
*/
function findChildren(
parentId: string | number,
list: Record<string, any>[]
): Record<string, any>[] {
for (const item of list) {
if (item.value === parentId && item.children) {
return item.children;
}
if (item.children) {
const children = findChildren(parentId, item.children);
if (children.length) return children;
}
}
return [];
}
// 格式化显示选中项(显示最后一个子节点的 label
const displayFormat = (items: any) => {
return items.length > 0 ? items[items.length - 1].label : "";
};
</script>

View File

@ -0,0 +1,196 @@
# 1.4.2
新增
1. 新增`filterValue`属性,支持通过此关键词来搜索并筛选树结构的内容
# 1.4.1
修复
1. 修复单选 onlyRadioLeaf 时末级节点无法选中的 bug
# 1.4.0
版本调整
建议更新,但需要注意,异步数据的时候,后台需返回 leaf 字段来判断是否末项数据
1. **调整数据项格式,新增 `leaf` 字段,来判断是否为末节点**
2. **调整数据项格式,新增 `sort` 字段,来排序节点位置**
3. **注意:异步加载数据,当为末项的时候,需要服务端数据返回 `leaf` 字段**
4. 新增 `alwaysFirstLoad` ,即异步数据总会在第一次展开节点时,拉取一次后台数据,来比对是否一致
5. 拆分 `field` 属性,**注意: 1.5.0 版本后将移除 `field` 属性**
6. 新增 `labelField``field.label`,指定节点对象中某个属性为**标签**字段,默认`label`
7. 新增 `valueField``field.key`,指定节点对象中某个属性为**值**字段,默认`value`
8. 新增 `childrenField``field.children`,指定节点对象中某个属性为**子树节点**字段,默认`children`
9. 新增 `disabledField``field.disabled`,指定节点对象中某个属性为**禁用**字段,默认`disabled`
10. 新增 `appendField``field.append`,指定节点对象中某个属性为**副标签**字段,默认`append`
11. 新增 `leafField``field.label`,指定节点对象中某个属性为**末级节点**字段,默认`leaf`
12. 新增 `sortField``field.label`,指定节点对象中某个属性为**排序**字段,默认`sort`
13. 新增 `isLeafFn` ,用来自定义控制数据项的末项
14. 更多的项目示例
15. 支持单选取消选中
16. 修复节点展开时可能存在的 bug
17. 修复节点选择可能存在的 bug
18. 调整为子节点默认继承父节点禁用属性
19. `setExpandedKeys` 添加参数一为 `all` 即可支持一键展开/收起全部节点
20. 其它更多优化
# 1.3.4
优化
1. 优化图标字体命名
# 1.3.3
优化
1. 新增方法调用
> - 新增`getUncheckedKeys`,返回未选的 key
> - 新增`getUncheckedNodes`,返回未选的节点
> - 新增`getUnexpandedKeys`,返回未展开的 key
> - 新增`getUnexpandedNodes`,返回未展开的节点
2. 优化示例项目
# 1.3.2
修复
1. 修复在 APP 真机环境中的报错
# 1.3.1
修复
1. 修复方法`setExpandedKeys`没联动展开上级父子节点
# 1.3.0
优化
1. `field`新增字段 `append` 用于在标签后面显示小提示
2. 新增支持点击标签也能选中节点
3. 方法`setExpandedKeys`支持加载动态数据
4. 修复父节点禁用,则不能展开及图标展开显示
5. 修复动态加载数据时,末级节点的 `children``null` 时仍显示展开图标
# 1.2.6
新增
1. 新增支持主题换色
2. 支持单选的`onlyRadioLeaf``true`时可点父节点展开/收起
3. 优化`expandChecked`调整为不展开无子节点的节点
# 1.2.5
新增
1. 新增 `expandChecked`,控制选择时是否展开当前已选的所有下级节点
# 1.2.4
修复
1. 修复动态数据展开状态异常问题
# 1.2.3
新增
1. 新增 `checkedDisabled`,是否渲染禁用值
2. 新增 `packDisabledkey`,是否返回已禁用并选中的 key
3. 修复选择父级时,子级已禁用但仍被选中的问题
# 1.2.2
优化
1. 调整动态数据载入处理方式
2. 修复节点数据因动态数据引起的状态异常
3. 修复初始节点数据默认选中
# 1.2.1
修复
1. 修复切换`选中状态`被重复选中问题
2. 修复动态数据引起的重复选择问题
# 1.2.0
新增
1. 新增方法调用
> - 新增`setCheckedKeys`,方法设置指定 key 的节点选中状态
> - 新增`setExpandedKeys`,方法设置指定 key 的节点展开状态
2. 修复小程序重复插槽一直刷报错问题
3. 优化展开时,会展开子级所以下级节点
# 1.1.1
新增
1. 新增`data``disabled`,支持节点禁用状态
2. 新增`field``disabled`,可自定`disabled`字段值
# 1.1.0
新增
1. 新增`loadMode``loadApi`,支持展开时加载异步数据
2. 新增方法调用
> - 新增`getCheckedKeys`,方法返回已选的 key
> - 新增`getHalfCheckedKeys`,方法返回半选的 key
> - 新增`getExpandedKeys`,方法返回已展开的 key
> - 新增`getCheckedNodes`,方法返回已选的节点
> - 新增`getHalfCheckedNodes`,方法返回半选的节点
> - 新增`getExpandedNodes`,方法返回已展开的节点
3. 对代码进行重构,更易于后期拓展
4. 此次更新后,页面多个的 DaTee 组件间的数据不再关联
# 1.0.6
新增
1. 新增`checkStrictly`,多选模式下选中时是否父子不关联
# 1.0.5
修复
1. 修复多选时已选数据重复问题
# 1.0.4
修复
1. 修复 `change` 事件回调数据的问题
# 1.0.3
优化
1. 优化文档及示例说明
# 1.0.2
新增
1. 新增 `onlyRadioLeaf` ,单选时只允许选中末级
2. 优化默认展开及默认选择的展开问题
# 1.0.1
新增
1. 支持展开/收起回调事件`@expand`
# 1.0.0
初始版本 1.0.0,基于 Vue3 进行开发,支持单选、多选,兼容各大平台
1. 支持单选
2. 支持多选

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,197 @@
export default {
/**
* 树的数据
*/
data: {
type: Array,
default: () => [],
},
/**
* 主题色
*/
themeColor: {
type: String,
default: "#007aff",
},
/**
* 是否开启多选,默认单选
*/
showCheckbox: {
type: Boolean,
default: false,
},
/**
* 默认选中的节点注意单选时为单个key多选时为key的数组
*/
defaultCheckedKeys: {
type: [Array, String, Number],
default: null,
},
/**
* 是否默认展开全部
*/
defaultExpandAll: {
type: Boolean,
default: false,
},
/**
* 默认展开的节点
*/
defaultExpandedKeys: {
type: Array,
default: null,
},
/**
* 筛选关键词
*/
filterValue: {
type: String,
default: "",
},
/**
* 是否自动展开到选中的节点,默认不展开
*/
expandChecked: {
type: Boolean,
default: false,
},
/**
* (旧)字段对应内容,默认为 {label: 'label',key: 'key', children: 'children', disabled: 'disabled', append: 'append'}
* 注意1.5.0版本后不再兼容
*/
field: {
type: Object,
default: null,
},
/**
* 标签字段(新,拆分了)
*/
labelField: {
type: String,
default: "label",
},
/**
* 值字段(新,拆分了)
*/
valueField: {
type: String,
default: "value",
},
/**
* 下级字段(新,拆分了)
*/
childrenField: {
type: String,
default: "children",
},
/**
* 禁用字段(新,拆分了)
*/
disabledField: {
type: String,
default: "disabled",
},
/**
* 末级节点字段(新,拆分了)
*/
leafField: {
type: String,
default: "leaf",
},
/**
* 副标签字段(新,拆分了)
*/
appendField: {
type: String,
default: "append",
},
/**
* 排序字段(新,拆分了)
*/
sortField: {
type: String,
default: "sort",
},
/**
* Api数据返回后的结果路径支持嵌套如`data.list`
*/
resultField: {
type: String,
default: "",
},
isLeafFn: {
type: Function,
default: null,
},
/**
* 是否显示单选图标,默认显示
*/
showRadioIcon: {
type: Boolean,
default: true,
},
/**
* 单选时只允许选中末级,默认可随意选中
*/
onlyRadioLeaf: {
type: Boolean,
default: false,
},
/**
* 多选时,是否执行父子不关联的任意勾选,默认父子关联
*/
checkStrictly: {
type: Boolean,
default: false,
},
/**
* 为 true 时,空的 children 数组会显示展开图标
*/
loadMode: {
type: Boolean,
default: false,
},
/**
* 异步加载接口
*/
loadApi: {
type: Function,
default: null,
},
/**
* 是否总在首次的时候加载一下内容,来比对是否一致
*/
alwaysFirstLoad: {
type: Boolean,
default: false,
},
/**
* 是否渲染(操作)禁用值
*/
checkedDisabled: {
type: Boolean,
default: false,
},
/**
* 是否返回已禁用的但已选中的key
*/
packDisabledkey: {
type: Boolean,
default: true,
},
/**
* 选择框的位置,可选 left/right
*/
checkboxPlacement: {
type: String,
default: "left",
},
/**
* 子项缩进距离默认40单位rpx
*/
indent: {
type: Number,
default: 40,
},
};

View File

@ -0,0 +1,310 @@
# da-tree
一个基于 Vue3 的 tree(树)组件,同时支持主题换色,可能是最适合你的 tree(树)组件
组件一直在更新,遇到问题可在下方讨论。
`同时更新 Vue2 版本,在此查看 ===>` **[Vue2 版](https://ext.dcloud.net.cn/plugin?id=12692)**
### 关于使用
可在右侧的`使用 HBuilderX 导入插件``下载示例项目ZIP`,方便快速上手。
可通过下方的示例及文档说明,进一步了解使用组件相关细节参数。
插件地址https://ext.dcloud.net.cn/plugin?id=12384
### 组件示例
```jsx
<template>
<view>多选</view>
<view><button @click="doCheckedTree(['2'],true)">全选</button></view>
<view><button @click="doCheckedTree(['2'],false)">取消全选</button></view>
<view><button @click="doCheckedTree(['211','222'],true)">选中指定节点</button></view>
<view><button @click="doCheckedTree(['211','222'],false)">取消选中指定节点</button></view>
<view><button @click="doExpandTree('all',true)">展开全部节点</button></view>
<view><button @click="doExpandTree('all',false)">收起全部节点</button></view>
<view><button @click="doExpandTree(['22','23'],true)">展开节点</button></view>
<view><button @click="doExpandTree(['22','23'],false)">收起节点</button></view>
<DaTree
ref="DaTreeRef"
:data="roomTreeData"
labelField="name"
valueField="id"
defaultExpandAll
showCheckbox
:defaultCheckedKeys="defaultCheckedKeysValue"
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
<view>单选</view>
<DaTree
:data="roomTreeData"
labelField="name"
valueField="id"
defaultExpandAll
:defaultCheckedKeys="defaultCheckedKeysValue2"
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
<view>默认展开指定节点</view>
<DaTree
:data="roomTreeData"
labelField="name"
valueField="id"
showCheckbox
:defaultExpandedKeys="defaultExpandKeysValue3"
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
<view>异步加载数据</view>
<DaTree
:data="roomTreeData"
labelField="name"
valueField="id"
showCheckbox
loadMode
:loadApi="GetApiData"
defaultExpandAll
@change="handleTreeChange"
@expand="handleExpandChange"></DaTree>
</template>
```
```js
import { defineComponent, ref } from "vue";
/**
* 模拟创建一个接口数据
*/
function GetApiData(currentNode) {
const { key } = currentNode;
return new Promise((resolve) => {
setTimeout(() => {
// 模拟返回空数据
if (key.indexOf("-") > -1) {
return resolve(null);
// return resolve([])
}
return resolve([
{
id: `${key}-1`,
name: `行政部X${key}-1`,
},
{
id: `${key}-2`,
name: `财务部X${key}-2`,
append: "定义了末项数据",
leaf: true,
},
{
id: `${key}-3`,
name: `资源部X${key}-3`,
},
{
id: `${key}-4`,
name: `资源部X${key}-3`,
append: "被禁用,无展开图标",
disabled: true,
},
]);
}, 2000);
});
}
import DaTree from "@/components/da-tree/index.vue";
export default defineComponent({
components: { DaTree },
setup() {
const DaTreeRef = ref();
// key的类型必须对应树数据key的类型
const defaultCheckedKeysValue = ref(["211", "222"]);
const defaultCheckedKeysValue2 = ref("222");
const defaultExpandKeysValue3 = ref(["212", "231"]);
const roomTreeData = ref([
{
id: "2",
name: "行政中心",
children: [
{
id: "21",
name: "行政部",
children: [
{
id: "211",
name: "行政一部",
children: null,
},
{
id: "212",
name: "行政二部",
children: [],
disabled: true,
},
],
},
{
id: "22",
name: "财务部",
children: [
{
id: "221",
name: "财务一部",
children: [],
disabled: true,
},
{
id: "222",
name: "财务二部",
children: [],
},
],
},
{
id: "23",
name: "人力资源部",
children: [
{
id: "231",
name: "人力一部",
children: [],
},
{
id: "232",
name: "人力二部",
append: "更多示例,请下载示例项目查看",
},
],
},
],
},
]);
function doExpandTree(keys, expand) {
DaTreeRef.value?.setExpandedKeys(keys, expand);
const gek = DaTreeRef.value?.getExpandedKeys();
console.log("当前已展开的KEY ==>", gek);
}
function doCheckedTree(keys, checked) {
DaTreeRef.value?.setCheckedKeys(keys, checked);
const gek = DaTreeRef.value?.getCheckedKeys();
console.log("当前已选中的KEY ==>", gek);
}
function handleTreeChange(allSelectedKeys, currentItem) {
console.log("handleTreeChange ==>", allSelectedKeys, currentItem);
}
function handleExpandChange(expand, currentItem) {
console.log("handleExpandChange ==>", expand, currentItem);
}
return {
DaTreeRef,
roomTreeData,
defaultCheckedKeysValue,
defaultCheckedKeysValue2,
defaultExpandKeysValue3,
handleTreeChange,
handleExpandChange,
GetApiData,
doExpandTree,
doCheckedTree,
};
},
});
```
** 更多示例请下载/导入示例项目 ZIP 查看 **
### 组件参数
| 属性 | 类型 | 默认值 | 必填 | 说明 |
| :------------------ | :------------------------------ | :--------- | :--- | :--------------------------------------------------------------------------- |
| data | `Array` | - | 是 | 树的数据 |
| themeColor | `String` | `#007aff` | 否 | 主题色,十六进制 |
| defaultCheckedKeys | `Array` \| `Number` \| `String` | - | 否 | 默认选中的节点,单选为单个 key多选为 key 的数组 |
| showCheckbox | `Boolean` | `false` | 否 | 是否开启多选,默认单选 |
| checkStrictly | `Boolean` | `false` | 否 | 多选时,是否执行父子不关联的任意勾选,默认父子关联 |
| showRadioIcon | `Boolean` | `true` | 否 | 是否显示单选图标,默认显示 |
| onlyRadioLeaf | `Boolean` | `true` | 否 | 单选时只允许选中末级,默认可随意选中 |
| defaultExpandAll | `Boolean` | `false` | 否 | 是否默认展开全部 |
| defaultExpandedKeys | `Array` | - | 否 | 默认展开的节点 |
| indent | `Number` | `40` | 否 | 子项缩进距离,单位 rpx |
| checkboxPlacement | `String` | `left` | 否 | 选择框的位置,可选 left/right |
| loadMode | `Boolean` | `false` | 否 | 为 true 时,空的 children 数组会显示展开图标 |
| loadApi | `Function` | - | 否 | 选择框的位置,可选 left/right |
| checkedDisabled | `Boolean` | `false` | 否 | 是否渲染禁用值,默认不渲染 |
| packDisabledkey | `Boolean` | `true` | 否 | 是否返回已禁用的但已选中的 key默认返回禁用已选值 |
| expandChecked | `Boolean` | `false` | 否 | 是否自动展开到选中的节点,默认不展开 |
| alwaysFirstLoad | `Boolean` | `false` | 否 | 是否总在首次的时候加载一下内容,默认不加载,否则只有展开末级节点才会加载数据 |
| isLeafFn | `Function` | - | 否 | 自定义函数返回来控制数据项的末项 |
| field | `Object` | - | 否 | 字段对应内容,格式参考下方(1.5.0 后移除,请用单独的字段匹配) |
| labelField | `String` | `label` | 否 | 指定节点对象中某个属性为标签字段,默认`label` |
| valueField | `String` | `value` | 否 | 指定节点对象中某个属性为值字段,默认`value` |
| childrenField | `String` | `children` | 否 | 指定节点对象中某个属性为子树节点字段,默认`children` |
| disabledField | `String` | `disabled` | 否 | 指定节点对象中某个属性为禁用字段,默认`disabled` |
| appendField | `String` | `append` | 否 | 指定节点对象中某个属性为副标签字段,默认`append` |
| leafField | `String` | `leaf` | 否 | 指定节点对象中某个属性为末级节点字段,默认`leaf` |
| sortField | `String` | `sort` | 否 | 指定节点对象中某个属性为排序字段,默认`sort` |
| filterValue | `String` | - | 否 | 搜索筛选的关键词,通过输入关键词筛选内容 |
**field 格式(1.5.0 后移除,请用单独的字段匹配)**
```js
{
label: 'label',
key: 'key',
children: 'children',
disabled: 'disabled',
append: 'append'
}
```
### 组件事件
| 事件名称 | 回调参数 | 说明 |
| :------- | :-------------------------------------- | :-------------- |
| change | `(allCheckedKeys, currentItem) => void` | 选中时回调 |
| expand | `(expandState, currentItem) => void` | 展开/收起时回调 |
### 组件方法
| 方法名称 | 参数 | 说明 |
| :------------------ | :--------------- | :------------------------------------------------------------------------------------------------ |
| setCheckedKeys | `(keys,checked)` | 设置指定 key 的节点选中/取消选中的状态。注: keys 单选时为 key多选时为 key 的数组 |
| setExpandedKeys | `(keys,expand)` | 设置指定 key 的节点展开/收起的状态,当 keys 为 all 时即代表展开/收起全部。注keys 为数组或 `all` |
| getCheckedKeys | - | 返回已选的 key |
| getHalfCheckedKeys | - | 返回半选的 key |
| getUncheckedKeys | - | 返回未选的 key |
| getCheckedNodes | - | 返回已选的节点 |
| getUncheckedNodes | - | 返回未选的节点 |
| getHalfCheckedNodes | - | 返回半选的节点 |
| getExpandedKeys | - | 返回已展开的 key |
| getUnexpandedKeys | - | 返回未展开的 key |
| getExpandedNodes | - | 返回已展开的节点 |
| getUnexpandedNodes | - | 返回未展开的节点 |
### 组件版本
v1.4.2
### 差异化
已通过测试
> - H5 页面
> - 微信小程序
> - 支付宝、钉钉小程序
> - 字节跳动、抖音、今日头条小程序
> - 百度小程序
> - 飞书小程序
> - QQ 小程序
> - 京东小程序
未测试
> - 快手小程序由于非企业用户暂无演示
> - 快应用、360 小程序因 Vue3 支持的原因暂无演示
### 开发组
[@CRLANG](https://crlang.com)

View File

@ -0,0 +1,153 @@
/** 未选 */
export const unCheckedStatus = 0;
/** 半选 */
export const halfCheckedStatus = 1;
/** 选中 */
export const isCheckedStatus = 2;
/**
* 深拷贝内容
* @param originData 拷贝对象
* @author crlang(https://crlang.com)
*/
export function deepClone(originData: any): any {
const type = Object.prototype.toString.call(originData);
let data: any;
if (type === "[object Array]") {
data = [];
for (let i = 0; i < originData.length; i++) {
data.push(deepClone(originData[i]));
}
} else if (type === "[object Object]") {
data = {} as Record<string, any>;
for (const prop in originData) {
if (Object.prototype.hasOwnProperty.call(originData, prop)) {
// 非继承属性
data[prop] = deepClone(originData[prop]);
}
}
} else {
data = originData;
}
return data;
}
/**
* 获取所有指定的节点
* @param list 列表
* @param type 类型
* @param value 值
* @param packDisabledkey 是否包含禁用节点
* @author crlang(https://crlang.com)
*/
export function getAllNodes(list: any[], type: string, value: any, packDisabledkey = true): any[] {
if (!list || list.length === 0) {
return [];
}
const res = [];
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item[type] === value) {
if ((packDisabledkey && item.disabled) || !item.disabled) {
res.push(item);
}
}
}
return res;
}
/**
* 获取所有指定的key值
* @param list 列表
* @param type 类型
* @param value 值
* @param packDisabledkey 是否包含禁用节点
* @author crlang(https://crlang.com)
*/
export function getAllNodeKeys(
list: any[],
type: string,
value: any,
packDisabledkey = true
): string[] | null {
if (!list || list.length === 0) {
return null;
}
const res: string[] = [];
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (item[type] === value) {
if ((packDisabledkey && item.disabled) || !item.disabled) {
res.push(item.key);
}
}
}
return res.length ? res : null;
}
/**
* 错误输出
* @param msg 错误消息
* @param args 附加参数
*/
export function logError(msg: string, ...args: any[]): void {
console.error(`DaTree: ${msg}`, ...args);
}
const toString = Object.prototype.toString;
export function is(val: any, type: string): boolean {
return toString.call(val) === `[object ${type}]`;
}
/**
* 是否对象(Object)
* @param val 值
*/
export function isObject(val: any): boolean {
return val !== null && is(val, "Object");
}
/**
* 是否数字(Number)
* @param val 值
*/
export function isNumber(val: any): boolean {
return is(val, "Number");
}
/**
* 是否字符串(String)
* @param val 值
*/
export function isString(val: any): boolean {
return is(val, "String");
}
/**
* 是否函数方法(Function)
* @param val 值
*/
export function isFunction(val: any): boolean {
return typeof val === "function";
}
/**
* 是否布尔(Boolean)
* @param val 值
*/
export function isBoolean(val: any): boolean {
return is(val, "Boolean");
}
/**
* 是否数组(Array)
* @param val 值
*/
export function isArray(val: any): boolean {
return val && Array.isArray(val);
}

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,158 @@
<template>
<view class="container loading1">
<view class="shape shape1" />
<view class="shape shape2" />
<view class="shape shape3" />
<view class="shape shape4" />
</view>
</template>
<script>
export default {
name: "loading1",
data() {
return {};
},
};
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading1 {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #ee6666;
}
.loading1 .shape1 {
-webkit-animation: animation1shape1 0.5s ease 0s infinite alternate;
animation: animation1shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, 16px);
transform: translate(16px, 16px);
}
}
@keyframes animation1shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, 16px);
transform: translate(16px, 16px);
}
}
.loading1 .shape2 {
-webkit-animation: animation1shape2 0.5s ease 0s infinite alternate;
animation: animation1shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, 16px);
transform: translate(-16px, 16px);
}
}
@keyframes animation1shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, 16px);
transform: translate(-16px, 16px);
}
}
.loading1 .shape3 {
-webkit-animation: animation1shape3 0.5s ease 0s infinite alternate;
animation: animation1shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, -16px);
transform: translate(16px, -16px);
}
}
@keyframes animation1shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(16px, -16px);
transform: translate(16px, -16px);
}
}
.loading1 .shape4 {
-webkit-animation: animation1shape4 0.5s ease 0s infinite alternate;
animation: animation1shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation1shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, -16px);
transform: translate(-16px, -16px);
}
}
@keyframes animation1shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-16px, -16px);
transform: translate(-16px, -16px);
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<view class="container loading2">
<view class="shape shape1" />
<view class="shape shape2" />
<view class="shape shape3" />
<view class="shape shape4" />
</view>
</template>
<script>
export default {
name: "loading2",
data() {
return {};
},
};
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading2 {
-webkit-transform: rotate(10deg);
transform: rotate(10deg);
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading2 .shape {
border-radius: 5px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #ee6666;
}
.loading2 .shape1 {
-webkit-animation: animation2shape1 0.5s ease 0s infinite alternate;
animation: animation2shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
@keyframes animation2shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
.loading2 .shape2 {
-webkit-animation: animation2shape2 0.5s ease 0s infinite alternate;
animation: animation2shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
@keyframes animation2shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
.loading2 .shape3 {
-webkit-animation: animation2shape3 0.5s ease 0s infinite alternate;
animation: animation2shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
@keyframes animation2shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
.loading2 .shape4 {
-webkit-animation: animation2shape4 0.5s ease 0s infinite alternate;
animation: animation2shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation2shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
@keyframes animation2shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<view class="container loading3">
<view class="shape shape1" />
<view class="shape shape2" />
<view class="shape shape3" />
<view class="shape shape4" />
</view>
</template>
<script>
export default {
name: "loading3",
data() {
return {};
},
};
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading3 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading3 .shape1 {
border-top-left-radius: 10px;
}
.container.loading3 .shape2 {
border-top-right-radius: 10px;
}
.container.loading3 .shape3 {
border-bottom-left-radius: 10px;
}
.container.loading3 .shape4 {
border-bottom-right-radius: 10px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #ee6666;
}
.loading3 .shape1 {
-webkit-animation: animation3shape1 0.5s ease 0s infinite alternate;
animation: animation3shape1 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
@keyframes animation3shape1 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
.loading3 .shape2 {
-webkit-animation: animation3shape2 0.5s ease 0s infinite alternate;
animation: animation3shape2 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
@keyframes animation3shape2 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
.loading3 .shape3 {
-webkit-animation: animation3shape3 0.5s ease 0s infinite alternate;
animation: animation3shape3 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
@keyframes animation3shape3 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
.loading3 .shape4 {
-webkit-animation: animation3shape4 0.5s ease 0s infinite alternate;
animation: animation3shape4 0.5s ease 0s infinite alternate;
}
@-webkit-keyframes animation3shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
@keyframes animation3shape4 {
from {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
to {
-webkit-transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<view class="container loading5">
<view class="shape shape1" />
<view class="shape shape2" />
<view class="shape shape3" />
<view class="shape shape4" />
</view>
</template>
<script>
export default {
name: "loading5",
data() {
return {};
},
};
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading5 .shape {
width: 15px;
height: 15px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #ee6666;
}
.loading5 .shape1 {
animation: animation5shape1 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
-webkit-transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
@keyframes animation5shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
-webkit-transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
.loading5 .shape2 {
animation: animation5shape2 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
-webkit-transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
@keyframes animation5shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
-webkit-transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
-webkit-transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
.loading5 .shape3 {
animation: animation5shape3 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
-webkit-transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
@keyframes animation5shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
-webkit-transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
.loading5 .shape4 {
animation: animation5shape4 2s ease 0s infinite reverse;
}
@-webkit-keyframes animation5shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
-webkit-transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
@keyframes animation5shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
-webkit-transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
-webkit-transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
</style>

View File

@ -0,0 +1,226 @@
<template>
<view class="container loading6">
<view class="shape shape1" />
<view class="shape shape2" />
<view class="shape shape3" />
<view class="shape shape4" />
</view>
</template>
<script>
export default {
name: "loading6",
data() {
return {};
},
};
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading6 {
-webkit-animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading6 .shape {
width: 12px;
height: 12px;
border-radius: 2px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #ee6666;
}
.loading6 .shape1 {
-webkit-animation: animation6shape1 2s linear 0s infinite normal;
animation: animation6shape1 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
-webkit-transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
@keyframes animation6shape1 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
-webkit-transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
.loading6 .shape2 {
-webkit-animation: animation6shape2 2s linear 0s infinite normal;
animation: animation6shape2 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
-webkit-transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
@keyframes animation6shape2 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
-webkit-transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
-webkit-transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
.loading6 .shape3 {
-webkit-animation: animation6shape3 2s linear 0s infinite normal;
animation: animation6shape3 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
-webkit-transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
@keyframes animation6shape3 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
-webkit-transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
.loading6 .shape4 {
-webkit-animation: animation6shape4 2s linear 0s infinite normal;
animation: animation6shape4 2s linear 0s infinite normal;
}
@-webkit-keyframes animation6shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
-webkit-transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
@keyframes animation6shape4 {
0% {
-webkit-transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
-webkit-transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
-webkit-transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
-webkit-transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<view>
<Loading1 v-if="loadingType == 1" />
<Loading2 v-if="loadingType == 2" />
<Loading3 v-if="loadingType == 3" />
<Loading4 v-if="loadingType == 4" />
<Loading5 v-if="loadingType == 5" />
</view>
</template>
<script>
import Loading1 from "./loading1.vue";
import Loading2 from "./loading2.vue";
import Loading3 from "./loading3.vue";
import Loading4 from "./loading4.vue";
import Loading5 from "./loading5.vue";
export default {
name: "qiun-loading",
components: { Loading1, Loading2, Loading3, Loading4, Loading5 },
props: {
loadingType: {
type: Number,
default: 2,
},
},
data() {
return {};
},
};
</script>
<style></style>

View File

@ -0,0 +1,122 @@
<template>
<view :class="['todo-item', { completed: todo.completed }]" @tap="toggleCompleted">
<view class="status-icon">
<wd-icon v-if="todo.completed" name="check" color="#67C23A" size="20" />
<view v-else class="uncompleted-circle"></view>
</view>
<view class="todo-content">
<view class="todo-title">{{ todo.title }}</view>
<view v-if="todo.description" class="todo-description">
{{ todo.description }}
</view>
<view v-if="todo.deadline" class="todo-deadline">
<wd-icon name="time" size="14" color="#999999" style="margin-right: 4rpx" />
{{ formatDate(todo.deadline) }}
</view>
</view>
<view class="todo-actions">
<wd-icon
name="delete-filling"
color="#F56C6C"
size="18"
@tap.stop="$emit('delete', todo.id)"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
import { Todo } from "@/types/todo";
const props = defineProps({
todo: {
type: Object as () => Todo,
required: true,
},
});
const emit = defineEmits(["update", "delete"]);
// 切换完成状态
const toggleCompleted = () => {
emit("update", {
...props.todo,
completed: !props.todo.completed,
});
};
// 格式化日期
const formatDate = (date: string) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
</script>
<style lang="scss" scoped>
.todo-item {
display: flex;
align-items: flex-start;
padding: 24rpx;
margin-bottom: 16rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
&.completed {
opacity: 0.7;
.todo-title {
color: #909399;
text-decoration: line-through;
}
}
.status-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
.uncompleted-circle {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #dcdfe6;
border-radius: 50%;
}
}
.todo-content {
flex: 1;
.todo-title {
margin-bottom: 8rpx;
font-size: 30rpx;
font-weight: 500;
color: #303133;
}
.todo-description {
margin-bottom: 8rpx;
font-size: 26rpx;
color: #606266;
}
.todo-deadline {
display: flex;
align-items: center;
font-size: 24rpx;
color: #909399;
}
}
.todo-actions {
padding: 6rpx;
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<view class="todo-list">
<wd-empty v-if="todos.length === 0" description="暂无待办事项" />
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@update="handleUpdate"
@delete="handleDelete"
/>
</view>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";
import { Todo } from "@/types/todo";
import TodoItem from "@/components/todo/TodoItem.vue";
defineProps({
todos: {
type: Array as () => Todo[],
default: () => [],
},
});
const emit = defineEmits(["update", "delete"]);
// 处理更新待办事项
const handleUpdate = (todo: Todo) => {
emit("update", todo);
};
// 处理删除待办事项
const handleDelete = (id: string) => {
emit("delete", id);
};
</script>
<style lang="scss" scoped>
.todo-list {
padding: 20rpx 0;
}
</style>

View File

@ -0,0 +1,444 @@
/*
* uCharts®
* 高性能跨平台图表库支持H5、APP、小程序微信/支付宝/百度/头条/QQ/360、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 通用配置项
// 主题颜色配置如每个图表类型需要不同主题请在对应图表类型上更改color属性
const color = [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
];
const cfe = {
//demotype为自定义图表类型
type: [
"pie",
"ring",
"rose",
"funnel",
"line",
"column",
"area",
"radar",
"gauge",
"candle",
"demotype",
],
//增加自定义图表类型如果需要categories请在这里加入您的图表类型例如最后的"demotype"
categories: ["line", "column", "area", "radar", "gauge", "candle", "demotype"],
//instance为实例变量承载属性option为eopts承载属性不要删除
instance: {},
option: {},
//下面是自定义format配置因除H5端外的其他端无法通过props传递函数只能通过此属性对应下标的方式来替换
formatter: {
tooltipDemo1: function (res) {
let result = "";
for (let i in res) {
if (i == 0) {
result += res[i].axisValueLabel + "年销售额";
}
let value = "--";
if (res[i].data !== null) {
value = res[i].data;
}
// #ifdef H5
result += "\n" + res[i].seriesName + "" + value + " 万元";
// #endif
// #ifdef APP-PLUS
result += "<br/>" + res[i].marker + res[i].seriesName + "" + value + " 万元";
// #endif
}
return result;
},
legendFormat: function (name) {
return "自定义图例+" + name;
},
yAxisFormatDemo: function (value, index) {
return value + "元";
},
seriesFormatDemo: function (res) {
return res.name + "年" + res.value + "元";
},
},
//这里演示了自定义您的图表类型的option可以随意命名之后在组件上 type="demotype" 后组件会调用这个花括号里的option如果组件上还存在eopts参数会将demotype与eopts中option合并后渲染图表。
demotype: {
color: color,
//在这里填写echarts的option即可
},
//下面是自定义配置,请添加项目所需的通用配置
column: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "axis",
},
grid: {
top: 30,
bottom: 50,
right: 15,
left: 40,
},
legend: {
bottom: "left",
},
toolbox: {
show: false,
},
xAxis: {
type: "category",
axisLabel: {
color: "#666666",
},
axisLine: {
lineStyle: {
color: "#CCCCCC",
},
},
boundaryGap: true,
data: [],
},
yAxis: {
type: "value",
axisTick: {
show: false,
},
axisLabel: {
color: "#666666",
},
axisLine: {
lineStyle: {
color: "#CCCCCC",
},
},
},
seriesTemplate: {
name: "",
type: "bar",
data: [],
barwidth: 20,
label: {
show: true,
color: "#666666",
position: "top",
},
},
},
line: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "axis",
},
grid: {
top: 30,
bottom: 50,
right: 15,
left: 40,
},
legend: {
bottom: "left",
},
toolbox: {
show: false,
},
xAxis: {
type: "category",
axisLabel: {
color: "#666666",
},
axisLine: {
lineStyle: {
color: "#CCCCCC",
},
},
boundaryGap: true,
data: [],
},
yAxis: {
type: "value",
axisTick: {
show: false,
},
axisLabel: {
color: "#666666",
},
axisLine: {
lineStyle: {
color: "#CCCCCC",
},
},
},
seriesTemplate: {
name: "",
type: "line",
data: [],
barwidth: 20,
label: {
show: true,
color: "#666666",
position: "top",
},
},
},
area: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "axis",
},
grid: {
top: 30,
bottom: 50,
right: 15,
left: 40,
},
legend: {
bottom: "left",
},
toolbox: {
show: false,
},
xAxis: {
type: "category",
axisLabel: {
color: "#666666",
},
axisLine: {
lineStyle: {
color: "#CCCCCC",
},
},
boundaryGap: true,
data: [],
},
yAxis: {
type: "value",
axisTick: {
show: false,
},
axisLabel: {
color: "#666666",
},
axisLine: {
lineStyle: {
color: "#CCCCCC",
},
},
},
seriesTemplate: {
name: "",
type: "line",
data: [],
areaStyle: {},
label: {
show: true,
color: "#666666",
position: "top",
},
},
},
pie: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "item",
},
grid: {
top: 40,
bottom: 30,
right: 15,
left: 15,
},
legend: {
bottom: "left",
},
seriesTemplate: {
name: "",
type: "pie",
data: [],
radius: "50%",
label: {
show: true,
color: "#666666",
position: "top",
},
},
},
ring: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "item",
},
grid: {
top: 40,
bottom: 30,
right: 15,
left: 15,
},
legend: {
bottom: "left",
},
seriesTemplate: {
name: "",
type: "pie",
data: [],
radius: ["40%", "70%"],
avoidLabelOverlap: false,
label: {
show: true,
color: "#666666",
position: "top",
},
labelLine: {
show: true,
},
},
},
rose: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "item",
},
legend: {
top: "bottom",
},
seriesTemplate: {
name: "",
type: "pie",
data: [],
radius: "55%",
center: ["50%", "50%"],
roseType: "area",
},
},
funnel: {
color: color,
title: {
text: "",
},
tooltip: {
trigger: "item",
formatter: "{b} : {c}%",
},
legend: {
top: "bottom",
},
seriesTemplate: {
name: "",
type: "funnel",
left: "10%",
top: 60,
bottom: 60,
width: "80%",
min: 0,
max: 100,
minSize: "0%",
maxSize: "100%",
sort: "descending",
gap: 2,
label: {
show: true,
position: "inside",
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: "solid",
},
},
itemStyle: {
bordercolor: "#fff",
borderwidth: 1,
},
emphasis: {
label: {
fontSize: 20,
},
},
data: [],
},
},
gauge: {
color: color,
tooltip: {
formatter: "{a} <br/>{b} : {c}%",
},
seriesTemplate: {
name: "业务指标",
type: "gauge",
detail: { formatter: "{value}%" },
data: [{ value: 50, name: "完成率" }],
},
},
candle: {
xAxis: {
data: [],
},
yAxis: {},
color: color,
title: {
text: "",
},
dataZoom: [
{
type: "inside",
xAxisIndex: [0, 1],
start: 10,
end: 100,
},
{
show: true,
xAxisIndex: [0, 1],
type: "slider",
bottom: 10,
start: 10,
end: 100,
},
],
seriesTemplate: {
name: "",
type: "k",
data: [],
},
},
};
export default cfe;

View File

@ -0,0 +1,676 @@
/*
* uCharts®
* 高性能跨平台图表库支持H5、APP、小程序微信/支付宝/百度/头条/QQ/360、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 主题颜色配置如每个图表类型需要不同主题请在对应图表类型上更改color属性
const color = [
"#1890FF",
"#91CB74",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3CA272",
"#FC8452",
"#9A60B4",
"#ea7ccc",
];
//事件转换函数主要用作格式化x轴为时间轴根据需求自行修改
const formatDateTime = (timeStamp, returnType) => {
var date = new Date();
date.setTime(timeStamp * 1000);
var y = date.getFullYear();
var m = date.getMonth() + 1;
m = m < 10 ? "0" + m : m;
var d = date.getDate();
d = d < 10 ? "0" + d : d;
var h = date.getHours();
h = h < 10 ? "0" + h : h;
var minute = date.getMinutes();
var second = date.getSeconds();
minute = minute < 10 ? "0" + minute : minute;
second = second < 10 ? "0" + second : second;
if (returnType == "full") {
return y + "-" + m + "-" + d + " " + h + ":" + minute + ":" + second;
}
if (returnType == "y-m-d") {
return y + "-" + m + "-" + d;
}
if (returnType == "h:m") {
return h + ":" + minute;
}
if (returnType == "h:m:s") {
return h + ":" + minute + ":" + second;
}
return [y, m, d, h, minute, second];
};
const cfu = {
//demotype为自定义图表类型一般不需要自定义图表类型只需要改根节点上对应的类型即可
type: [
"pie",
"ring",
"rose",
"word",
"funnel",
"map",
"arcbar",
"line",
"column",
"mount",
"bar",
"area",
"radar",
"gauge",
"candle",
"mix",
"tline",
"tarea",
"scatter",
"bubble",
"demotype",
],
range: [
"饼状图",
"圆环图",
"玫瑰图",
"词云图",
"漏斗图",
"地图",
"圆弧进度条",
"折线图",
"柱状图",
"山峰图",
"条状图",
"区域图",
"雷达图",
"仪表盘",
"K线图",
"混合图",
"时间轴折线",
"时间轴区域",
"散点图",
"气泡图",
"自定义类型",
],
//增加自定义图表类型如果需要categories请在这里加入您的图表类型例如最后的"demotype"
//自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴矢量x轴类图表没有categories不需要加入categories
categories: [
"line",
"column",
"mount",
"bar",
"area",
"radar",
"gauge",
"candle",
"mix",
"demotype",
],
//instance为实例变量承载属性不要删除
instance: {},
//option为opts及eopts承载属性不要删除
option: {},
//下面是自定义format配置因除H5端外的其他端无法通过props传递函数只能通过此属性对应下标的方式来替换
formatter: {
yAxisDemo1: function (val, index, opts) {
return val + "元";
},
yAxisDemo2: function (val, index, opts) {
return val.toFixed(2);
},
xAxisDemo1: function (val, index, opts) {
return val + "年";
},
xAxisDemo2: function (val, index, opts) {
return formatDateTime(val, "h:m");
},
seriesDemo1: function (val, index, series, opts) {
return val + "元";
},
tooltipDemo1: function (item, category, index, opts) {
if (index == 0) {
return "随便用" + item.data + "年";
} else {
return "其他我没改" + item.data + "天";
}
},
pieDemo: function (val, index, series, opts) {
if (index !== undefined) {
return series[index].name + "" + series[index].data + "元";
}
},
},
//这里演示了自定义您的图表类型的option可以随意命名之后在组件上 type="demotype" 后组件会调用这个花括号里的option如果组件上还存在opts参数会将demotype与opts中option合并后渲染图表。
demotype: {
//我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
type: "line",
color: color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: "dash",
dashLength: 2,
},
legend: {},
extra: {
line: {
type: "curve",
width: 2,
},
},
},
//下面是自定义配置,请添加项目所需的通用配置
pie: {
type: "pie",
color: color,
padding: [5, 5, 5, 5],
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: "#FFFFFF",
},
},
},
ring: {
type: "ring",
color: color,
padding: [5, 5, 5, 5],
rotate: false,
dataLabel: true,
legend: {
show: true,
position: "right",
lineHeight: 25,
},
title: {
name: "收益率",
fontSize: 15,
color: "#666666",
},
subtitle: {
name: "70%",
fontSize: 25,
color: "#7cb5ec",
},
extra: {
ring: {
ringWidth: 30,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: "#FFFFFF",
},
},
},
rose: {
type: "rose",
color: color,
padding: [5, 5, 5, 5],
legend: {
show: true,
position: "left",
lineHeight: 25,
},
extra: {
rose: {
type: "area",
minRadius: 50,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: false,
borderWidth: 2,
borderColor: "#FFFFFF",
},
},
},
word: {
type: "word",
color: color,
extra: {
word: {
type: "normal",
autoColors: false,
},
},
},
funnel: {
type: "funnel",
color: color,
padding: [15, 15, 0, 15],
extra: {
funnel: {
activeOpacity: 0.3,
activeWidth: 10,
border: true,
borderWidth: 2,
borderColor: "#FFFFFF",
fillOpacity: 1,
labelAlign: "right",
},
},
},
map: {
type: "map",
color: color,
padding: [0, 0, 0, 0],
dataLabel: true,
extra: {
map: {
border: true,
borderWidth: 1,
borderColor: "#666666",
fillOpacity: 0.6,
activeBorderColor: "#F04864",
activeFillColor: "#FACC14",
activeFillOpacity: 1,
},
},
},
arcbar: {
type: "arcbar",
color: color,
title: {
name: "百分比",
fontSize: 25,
color: "#00FF00",
},
subtitle: {
name: "默认标题",
fontSize: 15,
color: "#666666",
},
extra: {
arcbar: {
type: "default",
width: 12,
backgroundColor: "#E9E9E9",
startAngle: 0.75,
endAngle: 0.25,
gap: 2,
},
},
},
line: {
type: "line",
color: color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: "dash",
dashLength: 2,
},
legend: {},
extra: {
line: {
type: "straight",
width: 2,
activeType: "hollow",
},
},
},
tline: {
type: "line",
color: color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: false,
boundaryGap: "justify",
},
yAxis: {
gridType: "dash",
dashLength: 2,
data: [
{
min: 0,
max: 80,
},
],
},
legend: {},
extra: {
line: {
type: "curve",
width: 2,
activeType: "hollow",
},
},
},
tarea: {
type: "area",
color: color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: true,
boundaryGap: "justify",
},
yAxis: {
gridType: "dash",
dashLength: 2,
data: [
{
min: 0,
max: 80,
},
],
},
legend: {},
extra: {
area: {
type: "curve",
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: "hollow",
},
},
},
column: {
type: "column",
color: color,
padding: [15, 15, 0, 5],
xAxis: {
disableGrid: true,
},
yAxis: {
data: [{ min: 0 }],
},
legend: {},
extra: {
column: {
type: "group",
width: 30,
activeBgColor: "#000000",
activeBgOpacity: 0.08,
},
},
},
mount: {
type: "mount",
color: color,
padding: [15, 15, 0, 5],
xAxis: {
disableGrid: true,
},
yAxis: {
data: [{ min: 0 }],
},
legend: {},
extra: {
mount: {
type: "mount",
widthRatio: 1.5,
},
},
},
bar: {
type: "bar",
color: color,
padding: [15, 30, 0, 5],
xAxis: {
boundaryGap: "justify",
disableGrid: false,
min: 0,
axisLine: false,
},
yAxis: {},
legend: {},
extra: {
bar: {
type: "group",
width: 30,
meterBorde: 1,
meterFillColor: "#FFFFFF",
activeBgColor: "#000000",
activeBgOpacity: 0.08,
},
},
},
area: {
type: "area",
color: color,
padding: [15, 15, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: "dash",
dashLength: 2,
},
legend: {},
extra: {
area: {
type: "straight",
opacity: 0.2,
addLine: true,
width: 2,
gradient: false,
activeType: "hollow",
},
},
},
radar: {
type: "radar",
color: color,
padding: [5, 5, 5, 5],
dataLabel: false,
legend: {
show: true,
position: "right",
lineHeight: 25,
},
extra: {
radar: {
gridType: "radar",
gridColor: "#CCCCCC",
gridCount: 3,
opacity: 0.2,
max: 200,
labelShow: true,
},
},
},
gauge: {
type: "gauge",
color: color,
title: {
name: "66Km/H",
fontSize: 25,
color: "#2fc25b",
offsetY: 50,
},
subtitle: {
name: "实时速度",
fontSize: 15,
color: "#1890ff",
offsetY: -50,
},
extra: {
gauge: {
type: "default",
width: 30,
labelColor: "#666666",
startAngle: 0.75,
endAngle: 0.25,
startNumber: 0,
endNumber: 100,
labelFormat: "",
splitLine: {
fixRadius: 0,
splitNumber: 10,
width: 30,
color: "#FFFFFF",
childNumber: 5,
childWidth: 12,
},
pointer: {
width: 24,
color: "auto",
},
},
},
},
candle: {
type: "candle",
color: color,
padding: [15, 15, 0, 15],
enableScroll: true,
enableMarkLine: true,
dataLabel: false,
xAxis: {
labelCount: 4,
itemCount: 40,
disableGrid: true,
gridColor: "#CCCCCC",
gridType: "solid",
dashLength: 4,
scrollShow: true,
scrollAlign: "left",
scrollColor: "#A6A6A6",
scrollBackgroundColor: "#EFEBEF",
},
yAxis: {},
legend: {},
extra: {
candle: {
color: {
upLine: "#f04864",
upFill: "#f04864",
downLine: "#2fc25b",
downFill: "#2fc25b",
},
average: {
show: true,
name: ["MA5", "MA10", "MA30"],
day: [5, 10, 20],
color: ["#1890ff", "#2fc25b", "#facc14"],
},
},
markLine: {
type: "dash",
dashLength: 5,
data: [
{
value: 2150,
lineColor: "#f04864",
showLabel: true,
},
{
value: 2350,
lineColor: "#f04864",
showLabel: true,
},
],
},
},
},
mix: {
type: "mix",
color: color,
padding: [15, 15, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
disabled: false,
disableGrid: false,
splitNumber: 5,
gridType: "dash",
dashLength: 4,
gridColor: "#CCCCCC",
padding: 10,
showTitle: true,
data: [],
},
legend: {},
extra: {
mix: {
column: {
width: 20,
},
},
},
},
scatter: {
type: "scatter",
color: color,
padding: [15, 15, 0, 15],
dataLabel: false,
xAxis: {
disableGrid: false,
gridType: "dash",
splitNumber: 5,
boundaryGap: "justify",
min: 0,
},
yAxis: {
disableGrid: false,
gridType: "dash",
},
legend: {},
extra: {
scatter: {},
},
},
bubble: {
type: "bubble",
color: color,
padding: [15, 15, 0, 15],
xAxis: {
disableGrid: false,
gridType: "dash",
splitNumber: 5,
boundaryGap: "justify",
min: 0,
max: 250,
},
yAxis: {
disableGrid: false,
gridType: "dash",
data: [
{
min: 0,
max: 150,
},
],
},
legend: {},
extra: {
bubble: {
border: 2,
opacity: 0.5,
},
},
},
};
export default cfu;

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,6 @@
# uCharts JSSDK说明
1、如不使用uCharts组件可直接引用u-charts.js打包编译后会`自动压缩`,压缩后体积约为`120kb`
2、如果120kb的体积仍需压缩请手到uCharts官网通过在线定制选择您需要的图表。
3、config-ucharts.js为uCharts组件的用户配置文件升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
4、config-echarts.js为ECharts组件的用户配置文件升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。

File diff suppressed because it is too large Load Diff

18
src/components/u-charts/u-charts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10
src/directive/index.ts Normal file
View File

@ -0,0 +1,10 @@
import type { App } from "vue";
import { hasPerm, hasRole } from "./permission";
// 全局注册 directive
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有组件中都可用
app.directive("hasPerm", hasPerm);
app.directive("hasRole", hasRole);
}

View File

@ -0,0 +1,69 @@
import type { Directive, DirectiveBinding } from "vue";
import { useUserStore } from "@/store";
/**
* 按钮权限
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredPerms = binding.value;
console.log("requiredPerms", requiredPerms);
if (!requiredPerms) {
return;
}
// 校验传入的权限值是否合法
if (!requiredPerms || (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))) {
throw new Error(
"需要提供权限标识例如v-has-perm=\"'sys:user:add'\" 或 v-has-perm=\"['sys:user:add', 'sys:user:edit']\""
);
}
const { roles = [], perms = [] } = useUserStore().userInfo || {};
// 超级管理员拥有所有权限
if (roles?.includes("ROOT")) {
return;
}
// 检查权限
const hasAuth = Array.isArray(requiredPerms)
? requiredPerms.some((perm) => perms.includes(perm))
: perms.includes(requiredPerms);
// 如果没有权限,移除该元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};
/**
* 角色权限指令
*/
export const hasRole: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredRoles = binding.value;
// 校验传入的角色值是否合法
if (!requiredRoles || (typeof requiredRoles !== "string" && !Array.isArray(requiredRoles))) {
throw new Error(
"需要提供角色标识例如v-has-role=\"'ADMIN'\" 或 v-has-role=\"['ADMIN', 'TEST']\""
);
}
const { roles = [] } = useUserStore().userInfo || {};
// 检查是否有对应角色权限
const hasAuth = Array.isArray(requiredRoles)
? requiredRoles.some((role) => roles.includes(role))
: roles.includes(requiredRoles);
// 如果没有权限,移除元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};

View File

@ -0,0 +1,18 @@
/**
* 响应码枚举
*/
export const enum ResultCodeEnum {
/**
* 成功
*/
SUCCESS = "00000",
/**
* 错误
*/
ERROR = "B0001",
/**
* 令牌无效或过期
*/
TOKEN_INVALID = "A0230",
}

7
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

7
src/hooks/index.ts Normal file
View File

@ -0,0 +1,7 @@
/**
* 全局Hooks入口文件
* 导出所有可用的Hooks
*/
// 导出WebSocket相关Hook
export * from "./websocket";

View File

@ -0,0 +1,285 @@
import { getToken as getAccessToken } from "@/utils/cache";
export interface UseStompOptions {
/** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */
brokerURL?: string;
/** 重连延迟,单位毫秒,默认为 8000 */
reconnectDelay?: number;
/** 连接超时时间,单位毫秒,默认为 10000 */
connectionTimeout?: number;
/** 是否开启指数退避重连策略 */
useExponentialBackoff?: boolean;
/** 最大重连次数,默认为 5 */
maxReconnectAttempts?: number;
/** 最大重连延迟,单位毫秒,默认为 60000 */
maxReconnectDelay?: number;
/** 是否开启调试日志 */
debug?: boolean;
}
/**
* STOMP WebSocket连接Hook (UniApp版本)
* 用于管理WebSocket连接的建立、断开、重连和消息订阅
*/
export function useStomp(options: UseStompOptions = {}) {
// 默认配置
const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || "";
const brokerURL = ref(options.brokerURL ?? defaultBrokerURL);
const reconnectDelay = options.reconnectDelay ?? 8000;
const connectionTimeout = options.connectionTimeout ?? 10000;
const useExponentialBackoff = options.useExponentialBackoff ?? false;
const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
const maxReconnectDelay = options.maxReconnectDelay ?? 60000;
// 连接状态和计数
const isConnected = ref(false);
const reconnectCount = ref(0);
let reconnectTimer: number | null = null;
let connectionTimeoutTimer: number | null = null;
// 存储所有订阅
const subscriptions = ref<Record<string, any>>({});
// WebSocket实例
let socketTask: any = null;
const client = ref<any>(null);
/**
* 创建WebSocket连接
*/
const createSocketConnection = () => {
const token = getAccessToken();
if (!token) {
console.error("WebSocket连接失败未找到有效token");
return null;
}
// 创建WebSocket连接
try {
// 构建带有token的URL
let url = brokerURL.value;
if (url.indexOf("?") > -1) {
url += "&token=" + token;
} else {
url += "?token=" + token;
}
// 创建WebSocket连接
socketTask = uni.connectSocket({
url: url,
complete: () => {},
});
if (!socketTask) {
console.error("WebSocket连接创建失败");
return null;
}
// 设置WebSocket事件处理函数
socketTask.onOpen(() => {
isConnected.value = true;
reconnectCount.value = 0;
if (connectionTimeoutTimer) clearTimeout(connectionTimeoutTimer);
console.log("WebSocket连接已建立");
});
socketTask.onClose(() => {
isConnected.value = false;
console.log("WebSocket连接已关闭");
// 如果使用指数退避重连策略,则处理重连
if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) {
handleReconnect();
}
});
socketTask.onError((error: any) => {
console.error("WebSocket连接错误:", error);
});
socketTask.onMessage((res: any) => {
const message = JSON.parse(res.data);
// 处理订阅消息
if (message.subscription && subscriptions.value[message.subscription]) {
const subscription = subscriptions.value[message.subscription];
if (subscription.callback) {
subscription.callback(message);
}
}
});
return socketTask;
} catch (error) {
console.error("创建WebSocket连接时出错:", error);
return null;
}
};
/**
* 处理重连逻辑
*/
const handleReconnect = () => {
if (reconnectCount.value >= maxReconnectAttempts) {
console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`);
return;
}
reconnectCount.value++;
console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
// 使用指数退避策略
const delay = useExponentialBackoff
? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay)
: reconnectDelay;
// 清除之前的计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
// 设置重连计时器
reconnectTimer = setTimeout(() => {
if (!isConnected.value) {
connect();
}
}, delay) as unknown as number;
};
/**
* 建立WebSocket连接
*/
const connect = () => {
if (isConnected.value) {
return;
}
// 创建WebSocket连接
socketTask = createSocketConnection();
client.value = socketTask;
// 设置连接超时
if (connectionTimeoutTimer) {
clearTimeout(connectionTimeoutTimer);
}
connectionTimeoutTimer = setTimeout(() => {
if (!isConnected.value) {
console.warn("WebSocket连接超时");
if (useExponentialBackoff) {
handleReconnect();
}
}
}, connectionTimeout) as unknown as number;
};
/**
* 订阅主题
* @param destination 主题地址
* @param callback 回调函数
* @returns 订阅ID
*/
const subscribe = (destination: string, callback: (message: any) => void): string => {
if (!socketTask || !isConnected.value) {
console.warn("WebSocket未连接无法订阅:", destination);
return "";
}
// 生成唯一订阅ID
const subscriptionId = "sub-" + Math.random().toString(36).substr(2, 9);
// 发送订阅消息
socketTask.send({
data: JSON.stringify({
command: "SUBSCRIBE",
headers: {
id: subscriptionId,
destination: destination,
},
}),
success: () => {
console.log(`订阅成功: ${destination}, ID: ${subscriptionId}`);
},
fail: (err: any) => {
console.error(`订阅失败(${destination}):`, err);
},
});
// 保存订阅
subscriptions.value[subscriptionId] = {
destination,
callback,
};
return subscriptionId;
};
/**
* 取消订阅
* @param subscriptionId 订阅ID
*/
const unsubscribe = (subscriptionId: string) => {
if (!socketTask || !isConnected.value || !subscriptions.value[subscriptionId]) {
return;
}
try {
// 发送取消订阅消息
socketTask.send({
data: JSON.stringify({
command: "UNSUBSCRIBE",
headers: {
id: subscriptionId,
},
}),
success: () => {
console.log(`已取消订阅: ${subscriptionId}`);
},
fail: (err: any) => {
console.error(`取消订阅失败(${subscriptionId}):`, err);
},
});
} catch (error) {
console.error(`取消订阅失败(${subscriptionId}):`, error);
} finally {
Reflect.deleteProperty(subscriptions.value, subscriptionId);
}
};
/**
* 断开WebSocket连接
*/
const disconnect = () => {
if (!socketTask) {
return;
}
// 取消所有订阅
Object.keys(subscriptions.value).forEach(unsubscribe);
// 断开WebSocket连接
try {
socketTask.close({
success: () => {
console.log("WebSocket连接已断开");
isConnected.value = false;
socketTask = null;
},
fail: (err: any) => {
console.error("断开WebSocket连接失败:", err);
},
});
} catch (error) {
console.error("断开WebSocket连接失败:", error);
}
};
// 返回公开的API
return {
isConnected,
client,
connect,
disconnect,
subscribe,
unsubscribe,
};
}

View File

@ -0,0 +1,10 @@
/**
* WebSocket相关Hook入口文件
* 统一导出所有WebSocket相关Hook
*/
// 核心基础Hook
export { useStomp } from "./core/useStomp";
// 业务服务Hook
export { useDictSync } from "./services/useDictSync";

View File

@ -0,0 +1,188 @@
import { useDictStore } from "@/store/modules/dict";
import { useStomp } from "../core/useStomp";
import { ref } from "vue";
// 字典消息类型
export interface DictMessage {
dictCode: string;
timestamp: number;
}
// 字典事件回调类型
export type DictMessageCallback = (message: DictMessage) => void;
// 全局单例实例
let instance: ReturnType<typeof createDictSyncHook> | null = null;
/**
* 创建字典同步Hook
* 负责监听后端字典变更并同步到前端
*/
function createDictSyncHook() {
const dictStore = useDictStore();
// 使用现有的useStomp配置适合字典场景的重连参数
const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp({
reconnectDelay: 10000, // 使用更长的重连延迟 - 10秒
connectionTimeout: 15000, // 更长的连接超时时间 - 15秒
useExponentialBackoff: false, // 字典数据不需要指数退避策略
});
// 存储订阅ID
const subscriptionIds = ref<string[]>([]);
// 已订阅的主题
const subscribedTopics = ref<Set<string>>(new Set());
// 消息回调函数列表
const messageCallbacks = ref<DictMessageCallback[]>([]);
/**
* 注册字典消息回调
* @param callback 回调函数
*/
const onDictMessage = (callback: DictMessageCallback) => {
messageCallbacks.value.push(callback);
return () => {
// 返回取消注册的函数
const index = messageCallbacks.value.indexOf(callback);
if (index !== -1) {
messageCallbacks.value.splice(index, 1);
}
};
};
/**
* 初始化WebSocket
*/
const initWebSocket = async () => {
try {
// 连接WebSocket
connect();
// 设置字典订阅
setupDictSubscription();
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
}
};
/**
* 关闭WebSocket
*/
const closeWebSocket = () => {
// 取消所有订阅
subscriptionIds.value.forEach((id) => {
unsubscribe(id);
});
subscriptionIds.value = [];
subscribedTopics.value.clear();
// 断开连接
disconnect();
};
/**
* 设置字典订阅
*/
const setupDictSubscription = () => {
const topic = "/topic/dict";
// 防止重复订阅
if (subscribedTopics.value.has(topic)) {
console.log(`跳过重复订阅: ${topic}`);
return;
}
console.log(`开始尝试订阅字典主题: ${topic}`);
// 使用简化的重试逻辑依赖useStomp的连接管理
const attemptSubscribe = () => {
if (!isConnected.value) {
console.log("等待WebSocket连接建立...");
// 3秒后再次尝试
setTimeout(attemptSubscribe, 3000);
return;
}
// 检查是否已订阅
if (subscribedTopics.value.has(topic)) {
return;
}
console.log(`连接已建立,开始订阅: ${topic}`);
// 订阅字典更新
const subId = subscribe(topic, (message: any) => {
handleDictEvent(message);
});
if (subId) {
subscriptionIds.value.push(subId);
subscribedTopics.value.add(topic);
console.log(`字典主题订阅成功: ${topic}`);
} else {
console.warn(`字典主题订阅失败: ${topic}`);
}
};
// 开始尝试订阅
attemptSubscribe();
};
/**
* 处理字典事件
* @param message STOMP消息
*/
const handleDictEvent = (message: any) => {
if (!message.body) return;
try {
// 记录接收到的消息
console.log(`收到字典更新消息: ${message.body}`);
// 尝试解析消息
const parsedData = JSON.parse(message.body) as DictMessage;
const dictCode = parsedData.dictCode;
if (!dictCode) return;
// 清除缓存,等待按需加载
dictStore.removeDictItem(dictCode);
console.log(`字典缓存已清除: ${dictCode}`);
// 调用所有注册的回调函数
messageCallbacks.value.forEach((callback) => {
try {
callback(parsedData);
} catch (callbackError) {
console.error("[WebSocket] 回调执行失败:", callbackError);
}
});
// 显示提示消息
console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`);
} catch (error) {
console.error("[WebSocket] 解析消息失败:", error);
}
};
return {
isConnected,
initWebSocket,
closeWebSocket,
handleDictEvent,
onDictMessage,
};
}
/**
* 字典同步Hook
* 用于监听后端字典变更并同步到前端
*/
export function useDictSync() {
if (!instance) {
instance = createDictSyncHook();
}
return instance;
}

18
src/main.ts Normal file
View File

@ -0,0 +1,18 @@
import { createSSRApp } from "vue";
import App from "./App.vue";
import setupPlugins from "@/plugins";
import "uno.css";
import "@/styles/global.scss";
import { setupStore } from "@/store";
export function createApp() {
const app = createSSRApp(App);
setupStore(app);
app.use(setupPlugins);
return {
app,
};
}

72
src/manifest.json Normal file
View File

@ -0,0 +1,72 @@
{
"name": "",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios": {},
/* SDK */
"sdkConfigs": {}
}
},
/* */
"quickapp": {},
/* */
"mp-weixin": {
"appid": "wx99a151dc43d2637b",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3"
}

291
src/pages.json Normal file
View File

@ -0,0 +1,291 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
"^cu-(.*)": "@/components/cu-$1/index.vue"
}
},
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/work/index",
"style": {
"navigationBarTitleText": "工作台"
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/mine/about/index",
"style": {
"navigationBarTitleText": "关于我们"
}
},
{
"path": "pages/mine/faq/index",
"style": {
"navigationBarTitleText": "常见问题"
}
},
{
"path": "pages/mine/feedback/index",
"style": {
"navigationBarTitleText": "问题反馈"
}
},
{
"path": "pages/mine/settings/index",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "pages/mine/settings/account/index",
"style": {
"navigationBarTitleText": "账号和安全"
}
},
{
"path": "pages/mine/settings/agreement/index",
"style": {
"navigationBarTitleText": "用户协议"
}
},
{
"path": "pages/mine/settings/privacy/index",
"style": {
"navigationBarTitleText": "隐私政策"
}
},
{
"path": "pages/mine/settings/theme/index",
"style": {
"navigationBarTitleText": "主题设置"
}
},
{
"path": "pages/mine/settings/network/index",
"style": {
"navigationBarTitleText": "网络检测"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/mine/profile/index",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/work/user/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/work/log/index",
"style": {
"navigationBarTitleText": "日志管理"
}
},
{
"path": "pages/work/config/index",
"style": {
"navigationBarTitleText": "系统配置"
}
},
{
"path": "pages/work/notice/index",
"style": {
"navigationBarTitleText": "通知公告"
}
},
{
"path": "pages/work/notice/detail",
"style": {
"navigationBarTitleText": "通知详情"
}
},
{
"path": "pages/work/role/index",
"style": {
"navigationBarTitleText": "角色管理"
}
},
{
"path": "pages/work/role/assign-perm",
"style": {
"navigationBarTitleText": "角色分配权限"
}
},
{
"path": "pages/health/index/index",
"style": {
"navigationBarTitleText": "健康管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/health/consultation/index",
"style": {
"navigationBarTitleText": "在线问诊"
}
},
{
"path": "pages/health/consultation/doctor-list",
"style": {
"navigationBarTitleText": "选择医生"
}
},
{
"path": "pages/health/consultation/chat",
"style": {
"navigationBarTitleText": "在线咨询"
}
},
{
"path": "pages/health/detection/index",
"style": {
"navigationBarTitleText": "健康检测"
}
},
{
"path": "pages/health/detection/vitals",
"style": {
"navigationBarTitleText": "生命体征"
}
},
{
"path": "pages/health/detection/report",
"style": {
"navigationBarTitleText": "健康报告"
}
},
{
"path": "pages/health/chronic/index",
"style": {
"navigationBarTitleText": "慢病管理"
}
},
{
"path": "pages/health/chronic/diabetes",
"style": {
"navigationBarTitleText": "糖尿病管理"
}
},
{
"path": "pages/health/chronic/hypertension",
"style": {
"navigationBarTitleText": "高血压管理"
}
},
{
"path": "pages/health/diet/index",
"style": {
"navigationBarTitleText": "膳食管理"
}
},
{
"path": "pages/health/diet/record",
"style": {
"navigationBarTitleText": "饮食记录"
}
},
{
"path": "pages/health/constitution/index",
"style": {
"navigationBarTitleText": "体质辩识"
}
},
{
"path": "pages/health/constitution/test",
"style": {
"navigationBarTitleText": "体质测试"
}
},
{
"path": "pages/health/exercise/index",
"style": {
"navigationBarTitleText": "运动管理"
}
},
{
"path": "pages/health/exercise/record",
"style": {
"navigationBarTitleText": "运动记录"
}
},
{
"path": "pages/health/encyclopedia/index",
"style": {
"navigationBarTitleText": "中医百科"
}
},
{
"path": "pages/health/education/index",
"style": {
"navigationBarTitleText": "健康讲堂"
}
},
{
"path": "pages/health/profile/index",
"style": {
"navigationBarTitleText": "个人档案"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "vue-uniapp-template",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#474747",
"selectedColor": "#3B8DFF",
"backgroundColor": "#F8F8F8",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/health/index/index",
"text": "健康",
"iconPath": "static/tabbar/health.png",
"selectedIconPath": "static/tabbar/health-active.png"
},
// {
// "pagePath": "pages/work/index",
// "text": "工作台",
// "iconPath": "static/tabbar/work.png",
// "selectedIconPath": "static/tabbar/work-active.png"
// },
{
"pagePath": "pages/mine/index",
"text": "我的",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine-active.png"
}
]
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,532 @@
<template>
<view class="constitution-identification">
<!-- 当前体质状态 -->
<view v-if="userConstitution" class="current-constitution">
<view class="constitution-card">
<view class="constitution-header">
<text class="constitution-name">{{ userConstitution.name }}</text>
<text class="constitution-date">{{ formatDate(userConstitution.testDate) }}</text>
</view>
<view class="constitution-description">
<text>{{ userConstitution.description }}</text>
</view>
<view class="constitution-characteristics">
<text class="characteristics-title">主要特征</text>
<view class="characteristics-list">
<text
v-for="item in userConstitution.characteristics"
:key="item"
class="characteristic-item"
>
{{ item }}
</text>
</view>
</view>
<view class="constitution-score">
<text class="score-label">体质得分</text>
<text class="score-value">{{ userConstitution.score }}</text>
</view>
</view>
</view>
<!-- 体质调理建议 -->
<view class="constitution-advice">
<view class="section-title">调理建议</view>
<view class="advice-tabs">
<view
v-for="tab in adviceTabs"
:key="tab.key"
class="advice-tab"
:class="{ active: activeAdviceTab === tab.key }"
@click="switchAdviceTab(tab.key)"
>
<text>{{ tab.label }}</text>
</view>
</view>
<view class="advice-content">
<view v-for="advice in currentAdvices" :key="advice.id" class="advice-item">
<view class="advice-header">
<text class="advice-title">{{ advice.title }}</text>
<text class="advice-level" :class="advice.level">{{ advice.levelText }}</text>
</view>
<text class="advice-description">{{ advice.description }}</text>
</view>
</view>
</view>
<!-- 九种体质介绍 -->
<view class="constitution-types">
<view class="section-title">九种体质介绍</view>
<view class="constitution-grid">
<view
v-for="type in constitutionTypes"
:key="type.id"
class="constitution-type-card"
@click="viewConstitutionDetail(type)"
>
<view class="type-icon" :style="{ backgroundColor: type.color }">
<text>{{ type.name.charAt(0) }}</text>
</view>
<text class="type-name">{{ type.name }}</text>
<text class="type-description">{{ type.shortDesc }}</text>
</view>
</view>
</view>
<!-- 底部操作 -->
<view class="bottom-actions">
<button class="action-btn primary" @click="startTest">重新测试</button>
<button class="action-btn secondary" @click="viewHistory">测试历史</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
interface Constitution {
name: string;
description: string;
characteristics: string[];
score: number;
testDate: Date;
}
// interface AdviceItem {
// id: string;
// title: string;
// description: string;
// level: "high" | "medium" | "low";
// levelText: string;
// type: "diet" | "exercise" | "lifestyle" | "emotion";
// }
interface ConstitutionType {
id: string;
name: string;
shortDesc: string;
color: string;
description: string;
}
const userConstitution = ref<Constitution>({
name: "气虚体质",
description:
"气虚体质是指脏腑功能衰弱,气的推动、温煦、防御、固摄和气化功能减退,从而导致某些功能活动低下或衰退,抗病能力下降等衰弱的现象。",
characteristics: ["容易疲劳", "气短懒言", "易出汗", "舌淡苔白", "脉虚弱"],
score: 75,
testDate: new Date(),
});
const adviceTabs = [
{ key: "diet", label: "饮食调理" },
{ key: "exercise", label: "运动调理" },
{ key: "lifestyle", label: "生活调理" },
{ key: "emotion", label: "情志调理" },
];
const activeAdviceTab = ref("diet");
const adviceData = {
diet: [
{
id: "1",
title: "补气食材",
description: "多食用山药、大枣、蜂蜜、鸡肉等具有补气作用的食物",
level: "high",
levelText: "重要",
type: "diet",
},
{
id: "2",
title: "忌食寒凉",
description: "避免生冷寒凉食物,如冰饮、西瓜等",
level: "medium",
levelText: "注意",
type: "diet",
},
],
exercise: [
{
id: "3",
title: "温和运动",
description: "适宜散步、太极拳、八段锦等柔和的运动",
level: "high",
levelText: "推荐",
type: "exercise",
},
{
id: "4",
title: "避免剧烈运动",
description: "不宜进行大强度、长时间的运动",
level: "medium",
levelText: "注意",
type: "exercise",
},
],
lifestyle: [
{
id: "5",
title: "规律作息",
description: "保证充足睡眠,避免熬夜,建立规律的作息时间",
level: "high",
levelText: "重要",
type: "lifestyle",
},
],
emotion: [
{
id: "6",
title: "保持乐观",
description: "保持积极乐观的心态,避免过度忧虑",
level: "medium",
levelText: "建议",
type: "emotion",
},
],
};
const constitutionTypes = ref<ConstitutionType[]>([
{
id: "1",
name: "平和质",
shortDesc: "体质均衡",
color: "#4CAF50",
description: "身体健康,精力充沛",
},
{
id: "2",
name: "气虚质",
shortDesc: "容易疲劳",
color: "#FFC107",
description: "脏腑功能衰弱,气不足",
},
{
id: "3",
name: "阳虚质",
shortDesc: "畏寒怕冷",
color: "#FF9800",
description: "阳气不足,机体功能衰退",
},
{
id: "4",
name: "阴虚质",
shortDesc: "口干舌燥",
color: "#F44336",
description: "阴液不足,机体失润",
},
{
id: "5",
name: "痰湿质",
shortDesc: "形体肥胖",
color: "#9C27B0",
description: "痰湿凝聚,代谢异常",
},
{
id: "6",
name: "湿热质",
shortDesc: "面垢油腻",
color: "#E91E63",
description: "湿热内蕴,代谢异常",
},
{
id: "7",
name: "血瘀质",
shortDesc: "肤色晦暗",
color: "#3F51B5",
description: "血行不畅,瘀血内阻",
},
{
id: "8",
name: "气郁质",
shortDesc: "情绪低落",
color: "#009688",
description: "气机郁滞,情志异常",
},
{ id: "9", name: "特禀质", shortDesc: "先天异常", color: "#795548", description: "先天禀赋异常" },
]);
const currentAdvices = computed(() => {
return adviceData[activeAdviceTab.value as keyof typeof adviceData] || [];
});
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}${month}${day}`;
};
const switchAdviceTab = (tab: string) => {
activeAdviceTab.value = tab;
};
const startTest = () => {
uni.navigateTo({
url: "/pages/health/constitution/test",
});
};
const viewHistory = () => {
uni.navigateTo({
url: "/pages/health/constitution/history",
});
};
const viewConstitutionDetail = (type: ConstitutionType) => {
uni.navigateTo({
url: `/pages/health/constitution/detail?id=${type.id}`,
});
};
</script>
<style lang="scss" scoped>
.constitution-identification {
min-height: 100vh;
padding: 20rpx;
background-color: #f8f9fa;
}
.current-constitution {
margin-bottom: 20rpx;
}
.constitution-card {
padding: 30rpx;
color: white;
background: linear-gradient(135deg, #4caf50, #45a049);
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.3);
}
.constitution-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
.constitution-name {
font-size: 32rpx;
font-weight: 600;
}
.constitution-date {
font-size: 22rpx;
opacity: 0.8;
}
}
.constitution-description {
margin-bottom: 20rpx;
font-size: 24rpx;
line-height: 1.6;
opacity: 0.9;
}
.constitution-characteristics {
margin-bottom: 20rpx;
.characteristics-title {
margin-bottom: 12rpx;
font-size: 24rpx;
font-weight: 600;
}
.characteristics-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.characteristic-item {
padding: 8rpx 16rpx;
font-size: 22rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
}
}
}
.constitution-score {
display: flex;
align-items: center;
.score-label {
margin-right: 12rpx;
font-size: 24rpx;
}
.score-value {
font-size: 32rpx;
font-weight: 600;
}
}
.constitution-advice {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.section-title {
margin-bottom: 24rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.advice-tabs {
display: flex;
padding: 8rpx;
margin-bottom: 24rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.advice-tab {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 60rpx;
font-size: 24rpx;
color: #666;
border-radius: 8rpx;
transition: all 0.3s;
&.active {
color: white;
background: #4caf50;
}
}
.advice-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.advice-item {
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
.advice-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
.advice-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.advice-level {
padding: 6rpx 12rpx;
font-size: 20rpx;
border-radius: 16rpx;
&.high {
color: #f44336;
background: #ffebee;
}
&.medium {
color: #ff9800;
background: #fff3e0;
}
&.low {
color: #4caf50;
background: #e8f5e8;
}
}
}
.advice-description {
font-size: 24rpx;
line-height: 1.5;
color: #666;
}
}
.constitution-types {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.constitution-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.constitution-type-card {
padding: 20rpx;
text-align: center;
background: #f8f9fa;
border-radius: 12rpx;
transition: all 0.3s;
&:active {
transform: scale(0.95);
}
.type-icon {
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
margin: 0 auto 12rpx;
font-size: 28rpx;
font-weight: 600;
color: white;
border-radius: 50%;
}
.type-name {
display: block;
margin-bottom: 8rpx;
font-size: 24rpx;
font-weight: 600;
color: #333;
}
.type-description {
font-size: 20rpx;
color: #666;
}
}
.bottom-actions {
display: flex;
gap: 20rpx;
padding: 30rpx 0;
.action-btn {
flex: 1;
height: 88rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
&.primary {
color: white;
background: #4caf50;
}
&.secondary {
color: #666;
background: #f0f0f0;
}
}
}
</style>

View File

@ -0,0 +1,537 @@
<template>
<view class="constitution-test">
<!-- 测试进度 -->
<view class="test-progress">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progressWidth }"></view>
</view>
<text class="progress-text">{{ currentQuestionIndex + 1 }}/{{ questions.length }}</text>
</view>
<!-- 当前问题 -->
<view v-if="currentQuestion" class="question-container">
<view class="question-number">{{ currentQuestionIndex + 1 }}</view>
<view class="question-text">{{ currentQuestion.question }}</view>
<!-- 选项列表 -->
<view class="options-list">
<view
v-for="(option, index) in currentQuestion.options"
:key="index"
class="option-item"
:class="{ selected: selectedOption === index }"
@click="selectOption(index)"
>
<view class="option-radio">
<view v-if="selectedOption === index" class="radio-dot"></view>
</view>
<text class="option-text">{{ option.text }}</text>
</view>
</view>
</view>
<!-- 测试结果 -->
<view v-if="testCompleted" class="test-result">
<view class="result-header">
<text class="result-title">测试完成</text>
<text class="result-subtitle">您的体质类型是</text>
</view>
<view class="result-constitution">
<view class="constitution-icon" :style="{ backgroundColor: result.color }">
<text>{{ result.name.charAt(0) }}</text>
</view>
<text class="constitution-name">{{ result.name }}</text>
<text class="constitution-score">得分{{ result.score }}</text>
</view>
<view class="result-description">
<text>{{ result.description }}</text>
</view>
<view class="result-characteristics">
<text class="characteristics-title">主要特征</text>
<view class="characteristics-tags">
<text v-for="char in result.characteristics" :key="char" class="characteristic-tag">
{{ char }}
</text>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button
v-if="!testCompleted && currentQuestionIndex > 0"
class="action-btn secondary"
@click="previousQuestion"
>
上一题
</button>
<button
v-if="!testCompleted && selectedOption !== null"
class="action-btn primary"
@click="nextQuestion"
>
{{ isLastQuestion ? "完成测试" : "下一题" }}
</button>
<button v-if="testCompleted" class="action-btn primary full-width" @click="saveResult">
保存结果
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
interface QuestionOption {
text: string;
score: number;
constitution: string;
}
interface Question {
id: string;
question: string;
options: QuestionOption[];
}
interface TestResult {
name: string;
score: number;
description: string;
characteristics: string[];
color: string;
}
const currentQuestionIndex = ref(0);
const selectedOption = ref<number | null>(null);
const answers = ref<number[]>([]);
const testCompleted = ref(false);
const questions = ref<Question[]>([
{
id: "1",
question: "您平时是否容易疲劳?",
options: [
{ text: "非常容易疲劳,经常感到乏力", score: 5, constitution: "气虚" },
{ text: "比较容易疲劳", score: 3, constitution: "气虚" },
{ text: "偶尔疲劳", score: 2, constitution: "平和" },
{ text: "很少疲劳,精力充沛", score: 1, constitution: "平和" },
],
},
{
id: "2",
question: "您的睡眠质量如何?",
options: [
{ text: "经常失眠,睡眠质量很差", score: 5, constitution: "阴虚" },
{ text: "睡眠较浅,容易醒", score: 4, constitution: "阴虚" },
{ text: "睡眠一般", score: 2, constitution: "平和" },
{ text: "睡眠很好,很少失眠", score: 1, constitution: "平和" },
],
},
{
id: "3",
question: "您对寒冷的感受如何?",
options: [
{ text: "非常怕冷,手脚冰凉", score: 5, constitution: "阳虚" },
{ text: "比较怕冷", score: 3, constitution: "阳虚" },
{ text: "对温度变化不太敏感", score: 2, constitution: "平和" },
{ text: "不怕冷,甚至喜欢凉爽", score: 1, constitution: "湿热" },
],
},
{
id: "4",
question: "您的情绪状态通常如何?",
options: [
{ text: "经常感到抑郁、烦躁", score: 5, constitution: "气郁" },
{ text: "情绪起伏较大", score: 3, constitution: "气郁" },
{ text: "情绪比较稳定", score: 2, constitution: "平和" },
{ text: "心情愉快,很少烦躁", score: 1, constitution: "平和" },
],
},
{
id: "5",
question: "您的消化功能如何?",
options: [
{ text: "消化不良,经常腹胀", score: 5, constitution: "痰湿" },
{ text: "消化一般,偶有不适", score: 3, constitution: "痰湿" },
{ text: "消化功能正常", score: 2, constitution: "平和" },
{ text: "消化很好,食欲旺盛", score: 1, constitution: "平和" },
],
},
]);
const constitutionResults = {
气虚: {
name: "气虚体质",
description: "脏腑功能衰弱,气的推动、温煦、防御、固摄和气化功能减退",
characteristics: ["容易疲劳", "气短懒言", "易出汗", "舌淡苔白"],
color: "#FFC107",
},
阳虚: {
name: "阳虚体质",
description: "阳气不足,机体功能衰退,物质代谢活动减退",
characteristics: ["畏寒怕冷", "手脚冰凉", "精神不振", "舌淡胖"],
color: "#FF9800",
},
阴虚: {
name: "阴虚体质",
description: "阴液不足,机体失润,以脏腑失养为主要特征",
characteristics: ["口干舌燥", "五心烦热", "盗汗", "舌红少苔"],
color: "#F44336",
},
痰湿: {
name: "痰湿体质",
description: "痰湿凝聚,代谢异常,以形体肥胖为主要特征",
characteristics: ["形体肥胖", "腹部肥满", "口黏腻", "舌苔厚腻"],
color: "#9C27B0",
},
湿热: {
name: "湿热体质",
description: "湿热内蕴,以面垢油腻、口苦、苔黄腻为主要特征",
characteristics: ["面部油腻", "口苦口干", "身重困倦", "舌苔黄腻"],
color: "#E91E63",
},
气郁: {
name: "气郁体质",
description: "气机郁滞,神情抑郁,忧虑脆弱",
characteristics: ["情绪低落", "胸胁胀满", "多愁善感", "舌淡红"],
color: "#009688",
},
平和: {
name: "平和体质",
description: "阴阳气血调和,体质平和,身体健康",
characteristics: ["精力充沛", "睡眠良好", "食欲正常", "舌色淡红"],
color: "#4CAF50",
},
};
const result = ref<TestResult>({
name: "",
score: 0,
description: "",
characteristics: [],
color: "",
});
const currentQuestion = computed(() => {
return questions.value[currentQuestionIndex.value];
});
const progressWidth = computed(() => {
if (testCompleted.value) return "100%";
return `${((currentQuestionIndex.value + 1) / questions.value.length) * 100}%`;
});
const isLastQuestion = computed(() => {
return currentQuestionIndex.value === questions.value.length - 1;
});
const selectOption = (index: number) => {
selectedOption.value = index;
};
const nextQuestion = () => {
if (selectedOption.value === null) return;
answers.value[currentQuestionIndex.value] = selectedOption.value;
if (isLastQuestion.value) {
calculateResult();
testCompleted.value = true;
} else {
currentQuestionIndex.value++;
selectedOption.value = answers.value[currentQuestionIndex.value] ?? null;
}
};
const previousQuestion = () => {
if (currentQuestionIndex.value > 0) {
currentQuestionIndex.value--;
selectedOption.value = answers.value[currentQuestionIndex.value] ?? null;
}
};
const calculateResult = () => {
const constitutionScores: { [key: string]: number } = {};
answers.value.forEach((answerIndex, questionIndex) => {
const question = questions.value[questionIndex];
const selectedAnswer = question.options[answerIndex];
const constitution = selectedAnswer.constitution;
constitutionScores[constitution] =
(constitutionScores[constitution] || 0) + selectedAnswer.score;
});
// 找出得分最高的体质
let maxScore = 0;
let dominantConstitution = "平和";
Object.entries(constitutionScores).forEach(([constitution, score]) => {
if (score > maxScore) {
maxScore = score;
dominantConstitution = constitution;
}
});
const constitutionData =
constitutionResults[dominantConstitution as keyof typeof constitutionResults];
result.value = {
name: constitutionData.name,
score: Math.min(Math.round((maxScore / (questions.value.length * 5)) * 100), 100),
description: constitutionData.description,
characteristics: constitutionData.characteristics,
color: constitutionData.color,
};
};
const saveResult = () => {
// 保存测试结果
uni.showToast({
title: "结果已保存",
icon: "success",
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
};
</script>
<style lang="scss" scoped>
.constitution-test {
min-height: 100vh;
padding: 20rpx;
background-color: #f8f9fa;
}
.test-progress {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
.progress-bar {
height: 8rpx;
margin-bottom: 16rpx;
overflow: hidden;
background: #f0f0f0;
border-radius: 4rpx;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #45a049);
border-radius: 4rpx;
transition: width 0.3s ease;
}
}
.progress-text {
display: block;
font-size: 24rpx;
color: #666;
text-align: center;
}
}
.question-container {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.question-number {
margin-bottom: 16rpx;
font-size: 24rpx;
font-weight: 600;
color: #4caf50;
}
.question-text {
margin-bottom: 30rpx;
font-size: 28rpx;
font-weight: 500;
line-height: 1.6;
color: #333;
}
.options-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.option-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border: 2rpx solid transparent;
border-radius: 12rpx;
transition: all 0.3s;
&.selected {
background: #e8f5e8;
border-color: #4caf50;
}
.option-radio {
display: flex;
align-items: center;
justify-content: center;
width: 36rpx;
height: 36rpx;
margin-right: 20rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
.radio-dot {
width: 20rpx;
height: 20rpx;
background: #4caf50;
border-radius: 50%;
}
}
.option-text {
flex: 1;
font-size: 26rpx;
line-height: 1.5;
color: #333;
}
&.selected .option-radio {
border-color: #4caf50;
}
}
.test-result {
padding: 40rpx;
margin-bottom: 20rpx;
text-align: center;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.result-header {
margin-bottom: 30rpx;
.result-title {
display: block;
margin-bottom: 12rpx;
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.result-subtitle {
font-size: 24rpx;
color: #666;
}
}
.result-constitution {
margin-bottom: 30rpx;
.constitution-icon {
display: flex;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
margin: 0 auto 20rpx;
font-size: 40rpx;
font-weight: 600;
color: white;
border-radius: 50%;
}
.constitution-name {
display: block;
margin-bottom: 12rpx;
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.constitution-score {
font-size: 28rpx;
font-weight: 600;
color: #4caf50;
}
}
.result-description {
margin-bottom: 30rpx;
font-size: 26rpx;
line-height: 1.6;
color: #666;
text-align: left;
}
.result-characteristics {
text-align: left;
.characteristics-title {
display: block;
margin-bottom: 16rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.characteristics-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.characteristic-tag {
padding: 8rpx 16rpx;
font-size: 22rpx;
color: #4caf50;
background: #e8f5e8;
border-radius: 20rpx;
}
}
}
.bottom-actions {
display: flex;
gap: 20rpx;
padding: 30rpx 0;
.action-btn {
height: 88rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
&:not(.full-width) {
flex: 1;
}
&.full-width {
width: 100%;
}
&.primary {
color: white;
background: #4caf50;
}
&.secondary {
color: #666;
background: #f0f0f0;
}
}
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,478 @@
<template>
<view class="consultation-page">
<!-- 顶部搜索 -->
<view class="search-section">
<view class="search-bar">
<text class="search-icon">🔍</text>
<input v-model="searchKeyword" placeholder="搜索医生、科室或疾病" @confirm="handleSearch" />
</view>
</view>
<!-- 科室分类 -->
<view class="department-section">
<view class="section-title">科室分类</view>
<view class="department-grid">
<view
v-for="dept in departments"
:key="dept.id"
class="department-item"
@click="selectDepartment(dept)"
>
<view class="dept-icon">
<text class="icon">{{ dept.icon }}</text>
</view>
<text class="dept-name">{{ dept.name }}</text>
</view>
</view>
</view>
<!-- 在线医生 -->
<view class="doctor-section">
<view class="section-header">
<text class="section-title">在线医生</text>
<text class="more-link" @click="viewAllDoctors">查看全部</text>
</view>
<view class="doctor-list">
<view
v-for="doctor in onlineDoctors"
:key="doctor.id"
class="doctor-card"
@click="selectDoctor(doctor)"
>
<image class="doctor-avatar" :src="doctor.avatar" mode="aspectFill" />
<view class="doctor-info">
<view class="doctor-name">{{ doctor.name }}</view>
<view class="doctor-title">{{ doctor.title }}</view>
<view class="doctor-hospital">{{ doctor.hospital }}</view>
<view class="doctor-rating">
<text class="rating-text"> {{ doctor.rating }}</text>
<text class="experience">{{ doctor.experience }}年经验</text>
</view>
</view>
<view class="doctor-price">
<text class="price">¥{{ doctor.consultationFee }}</text>
<view class="online-status">在线</view>
</view>
</view>
</view>
</view>
<!-- 咨询历史 -->
<view class="history-section">
<view class="section-header">
<text class="section-title">咨询历史</text>
<text class="more-link" @click="viewHistory">查看全部</text>
</view>
<view class="history-list">
<view
v-for="consultation in consultationHistory"
:key="consultation.id"
class="history-item"
@click="viewConsultation(consultation)"
>
<image class="doctor-avatar" :src="consultation.doctorAvatar" mode="aspectFill" />
<view class="consultation-info">
<view class="doctor-name">{{ consultation.doctorName }}</view>
<view class="consultation-time">{{ formatTime(consultation.startTime) }}</view>
<view class="consultation-status" :class="consultation.status">
{{ getStatusText(consultation.status) }}
</view>
</view>
<view class="consultation-type">
<text class="type-icon">{{ getTypeIcon(consultation.type) }}</text>
</view>
</view>
</view>
</view>
<!-- 快速咨询入口 -->
<view class="quick-consultation">
<button class="quick-btn" @click="quickConsultation">
<text class="btn-icon"></text>
<text class="btn-text">快速咨询</text>
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
const searchKeyword = ref("");
const departments = ref([
{ id: 1, name: "内科", icon: "🫀" },
{ id: 2, name: "外科", icon: "🔪" },
{ id: 3, name: "妇科", icon: "👩" },
{ id: 4, name: "儿科", icon: "👶" },
{ id: 5, name: "骨科", icon: "🦴" },
{ id: 6, name: "皮肤科", icon: "🧴" },
{ id: 7, name: "眼科", icon: "👁️" },
{ id: 8, name: "口腔科", icon: "🦷" },
]);
const onlineDoctors = ref([
{
id: "1",
name: "张医生",
title: "主任医师",
hospital: "三甲医院",
avatar: "/static/images/doctor1.jpg",
rating: 4.8,
experience: 15,
consultationFee: 50,
},
{
id: "2",
name: "李医生",
title: "副主任医师",
hospital: "二甲医院",
avatar: "/static/images/doctor2.jpg",
rating: 4.6,
experience: 12,
consultationFee: 40,
},
]);
const consultationHistory = ref([
{
id: "1",
doctorName: "王医生",
doctorAvatar: "/static/images/doctor3.jpg",
startTime: new Date().toISOString(),
status: "completed",
type: "text",
},
]);
const handleSearch = () => {
console.log("搜索:", searchKeyword.value);
};
const selectDepartment = (dept: any) => {
uni.navigateTo({ url: `/pages/health/consultation/doctor-list?department=${dept.name}` });
};
const selectDoctor = (doctor: any) => {
uni.navigateTo({ url: `/pages/health/consultation/chat?doctorId=${doctor.id}` });
};
const viewAllDoctors = () => {
uni.navigateTo({ url: "/pages/health/consultation/doctor-list" });
};
const viewHistory = () => {
uni.navigateTo({ url: "/pages/health/consultation/history" });
};
const viewConsultation = (consultation: any) => {
uni.navigateTo({ url: `/pages/health/consultation/chat?consultationId=${consultation.id}` });
};
const quickConsultation = () => {
uni.navigateTo({ url: "/pages/health/consultation/quick" });
};
const formatTime = (time: string) => {
return new Date(time).toLocaleDateString("zh-CN");
};
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
pending: "待开始",
ongoing: "进行中",
completed: "已完成",
cancelled: "已取消",
};
return statusMap[status] || status;
};
const getTypeIcon = (type: string) => {
const typeMap: Record<string, string> = {
text: "💬",
voice: "🎤",
video: "📹",
};
return typeMap[type] || "💬";
};
</script>
<style lang="scss" scoped>
.consultation-page {
min-height: 100vh;
padding: 20rpx;
background: #f5f5f5;
}
.search-section {
margin-bottom: 30rpx;
.search-bar {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: white;
border-radius: 25rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.search-icon {
margin-right: 20rpx;
font-size: 30rpx;
color: #999;
}
input {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
}
.department-section {
padding: 30rpx;
margin-bottom: 30rpx;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.section-title {
margin-bottom: 30rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.department-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30rpx;
}
.department-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
transition: background 0.3s;
&:active {
background: #e9ecef;
}
.dept-icon {
margin-bottom: 15rpx;
.icon {
font-size: 40rpx;
}
}
.dept-name {
font-size: 24rpx;
color: #333;
text-align: center;
}
}
}
.doctor-section,
.history-section {
padding: 30rpx;
margin-bottom: 30rpx;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.more-link {
font-size: 26rpx;
color: #007aff;
}
}
}
.doctor-list {
.doctor-card {
display: flex;
align-items: center;
padding: 25rpx;
margin-bottom: 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
transition: background 0.3s;
&:active {
background: #e9ecef;
}
.doctor-avatar {
width: 100rpx;
height: 100rpx;
margin-right: 20rpx;
border-radius: 50%;
}
.doctor-info {
flex: 1;
.doctor-name {
margin-bottom: 8rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.doctor-title {
margin-bottom: 8rpx;
font-size: 24rpx;
color: #666;
}
.doctor-hospital {
margin-bottom: 8rpx;
font-size: 24rpx;
color: #999;
}
.doctor-rating {
display: flex;
align-items: center;
.rating-text {
margin-right: 20rpx;
font-size: 22rpx;
color: #ff9500;
}
.experience {
font-size: 22rpx;
color: #666;
}
}
}
.doctor-price {
text-align: right;
.price {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
font-weight: bold;
color: #ff3b30;
}
.online-status {
padding: 5rpx 15rpx;
font-size: 20rpx;
color: #34c759;
background: #e8f5e8;
border-radius: 10rpx;
}
}
}
}
.history-list {
.history-item {
display: flex;
align-items: center;
padding: 25rpx;
margin-bottom: 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
.doctor-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
border-radius: 50%;
}
.consultation-info {
flex: 1;
.doctor-name {
margin-bottom: 8rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.consultation-time {
margin-bottom: 8rpx;
font-size: 24rpx;
color: #666;
}
.consultation-status {
padding: 5rpx 15rpx;
font-size: 22rpx;
border-radius: 10rpx;
&.completed {
color: #34c759;
background: #e8f5e8;
}
&.ongoing {
color: #ff9500;
background: #fff2e8;
}
&.pending {
color: #007aff;
background: #e8f4fd;
}
}
}
.consultation-type {
.type-icon {
font-size: 30rpx;
}
}
}
}
.quick-consultation {
position: fixed;
right: 30rpx;
bottom: 100rpx;
.quick-btn {
display: flex;
align-items: center;
padding: 20rpx 40rpx;
background: linear-gradient(45deg, #ff6b6b, #ff8e8e);
border: none;
border-radius: 50rpx;
box-shadow: 0 8rpx 20rpx rgba(255, 107, 107, 0.3);
.btn-icon {
margin-right: 10rpx;
font-size: 32rpx;
}
.btn-text {
font-size: 28rpx;
font-weight: bold;
color: white;
}
}
}
</style>

View File

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

View File

@ -0,0 +1,612 @@
<template>
<view class="detection-page">
<!-- 顶部总览 -->
<view class="health-overview">
<view class="overview-header">
<text class="title">健康状态</text>
<text class="date">{{ formatDate(new Date()) }}</text>
</view>
<view class="health-score">
<view class="score-circle">
<text class="score-text">{{ healthScore }}</text>
<text class="score-label">健康评分</text>
</view>
<view class="score-desc">
<text class="status" :class="healthLevel">{{ getHealthStatus(healthScore) }}</text>
<text class="tips">{{ getHealthTips(healthScore) }}</text>
</view>
</view>
</view>
<!-- 快速检测 -->
<view class="quick-test-section">
<view class="section-title">快速检测</view>
<view class="test-grid">
<view v-for="test in quickTests" :key="test.id" class="test-item" @click="startTest(test)">
<view class="test-icon">
<text class="icon">{{ test.icon }}</text>
</view>
<text class="test-name">{{ test.name }}</text>
<text class="test-desc">{{ test.description }}</text>
</view>
</view>
</view>
<!-- 生命体征 -->
<view class="vital-signs">
<view class="section-header">
<text class="section-title">生命体征</text>
<text class="record-btn" @click="recordVitals">记录</text>
</view>
<view class="vital-grid">
<view
v-for="vital in vitalSigns"
:key="vital.type"
class="vital-item"
@click="viewVitalDetail(vital)"
>
<view class="vital-icon">
<text class="icon">{{ vital.icon }}</text>
</view>
<view class="vital-info">
<text class="vital-value">{{ vital.value }}</text>
<text class="vital-unit">{{ vital.unit }}</text>
<text class="vital-label">{{ vital.label }}</text>
</view>
<view class="vital-status" :class="vital.status">
<text class="status-text">{{ getStatusText(vital.status) }}</text>
</view>
</view>
</view>
</view>
<!-- 健康趋势 -->
<view class="health-trends">
<view class="section-title">健康趋势</view>
<view class="trend-chart">
<view class="chart-placeholder">
<text class="chart-text">📊 健康趋势图表</text>
</view>
<view class="trend-summary">
<view class="trend-item">
<text class="trend-label">本周平均</text>
<text class="trend-value">良好</text>
</view>
<view class="trend-item">
<text class="trend-label">改善指标</text>
<text class="trend-value">3</text>
</view>
</view>
</view>
</view>
<!-- 健康报告 -->
<view class="health-reports">
<view class="section-header">
<text class="section-title">健康报告</text>
<text class="more-link" @click="viewAllReports">查看全部</text>
</view>
<view class="report-list">
<view
v-for="report in recentReports"
:key="report.id"
class="report-item"
@click="viewReport(report)"
>
<view class="report-icon">
<text class="icon">📋</text>
</view>
<view class="report-info">
<text class="report-title">{{ report.title }}</text>
<text class="report-date">{{ formatDate(report.date) }}</text>
</view>
<view class="report-score" :class="report.level">
<text class="score">{{ report.score }}</text>
</view>
</view>
</view>
</view>
<!-- 生成报告按钮 -->
<view class="generate-report">
<button class="generate-btn" @click="generateReport">
<text class="btn-icon">📊</text>
<text class="btn-text">生成健康报告</text>
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
const healthScore = ref(85);
const healthLevel = ref("good");
const quickTests = ref([
{ id: 1, name: "血压测量", icon: "🩸", description: "监测血压状况" },
{ id: 2, name: "心率检测", icon: "❤️", description: "实时心率监控" },
{ id: 3, name: "体温测量", icon: "🌡️", description: "体温健康检查" },
{ id: 4, name: "BMI计算", icon: "⚖️", description: "身体质量指数" },
]);
const vitalSigns = ref([
{
type: "blood_pressure",
label: "血压",
value: "120/80",
unit: "mmHg",
icon: "🩸",
status: "normal",
},
{ type: "heart_rate", label: "心率", value: "72", unit: "bpm", icon: "❤️", status: "normal" },
{ type: "temperature", label: "体温", value: "36.5", unit: "°C", icon: "🌡️", status: "normal" },
{ type: "weight", label: "体重", value: "65.0", unit: "kg", icon: "⚖️", status: "normal" },
]);
const recentReports = ref([
{ id: "1", title: "综合健康报告", date: new Date(), score: 85, level: "good" },
{
id: "2",
title: "基础体检报告",
date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
score: 78,
level: "normal",
},
]);
const startTest = (test: any) => {
uni.navigateTo({ url: `/pages/health/detection/${test.type}?testId=${test.id}` });
};
const recordVitals = () => {
uni.navigateTo({ url: "/pages/health/detection/vitals" });
};
const viewVitalDetail = (vital: any) => {
uni.navigateTo({ url: `/pages/health/detection/vital-detail?type=${vital.type}` });
};
const viewAllReports = () => {
uni.navigateTo({ url: "/pages/health/detection/reports" });
};
const viewReport = (report: any) => {
uni.navigateTo({ url: `/pages/health/detection/report-detail?id=${report.id}` });
};
const generateReport = () => {
uni.showLoading({ title: "生成中..." });
setTimeout(() => {
uni.hideLoading();
uni.navigateTo({ url: "/pages/health/detection/generate-report" });
}, 2000);
};
const formatDate = (date: Date) => {
return date.toLocaleDateString("zh-CN");
};
const getHealthStatus = (score: number) => {
if (score >= 90) return "优秀";
if (score >= 80) return "良好";
if (score >= 70) return "一般";
return "需改善";
};
const getHealthTips = (score: number) => {
if (score >= 90) return "继续保持健康的生活方式";
if (score >= 80) return "整体健康状况良好,建议定期检查";
if (score >= 70) return "需要注意某些健康指标";
return "建议咨询专业医生意见";
};
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
normal: "正常",
warning: "注意",
danger: "异常",
};
return statusMap[status] || status;
};
</script>
<style lang="scss" scoped>
.detection-page {
min-height: 100vh;
padding: 20rpx;
background: #f5f5f5;
}
.health-overview {
padding: 30rpx;
margin-bottom: 30rpx;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
.overview-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.title {
font-size: 36rpx;
font-weight: bold;
}
.date {
font-size: 24rpx;
opacity: 0.8;
}
}
.health-score {
display: flex;
align-items: center;
.score-circle {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 150rpx;
height: 150rpx;
margin-right: 40rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
.score-text {
font-size: 48rpx;
font-weight: bold;
line-height: 1;
}
.score-label {
margin-top: 5rpx;
font-size: 20rpx;
opacity: 0.8;
}
}
.score-desc {
flex: 1;
.status {
display: block;
margin-bottom: 15rpx;
font-size: 32rpx;
font-weight: bold;
&.good {
color: #34c759;
}
&.normal {
color: #ff9500;
}
&.poor {
color: #ff3b30;
}
}
.tips {
font-size: 24rpx;
line-height: 1.4;
opacity: 0.9;
}
}
}
}
.quick-test-section {
padding: 30rpx;
margin-bottom: 30rpx;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.section-title {
margin-bottom: 30rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.test-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.test-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
transition: transform 0.3s;
&:active {
transform: scale(0.95);
}
.test-icon {
margin-bottom: 15rpx;
.icon {
font-size: 40rpx;
}
}
.test-name {
margin-bottom: 10rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.test-desc {
font-size: 22rpx;
color: #666;
text-align: center;
}
}
}
.vital-signs {
padding: 30rpx;
margin-bottom: 30rpx;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.record-btn {
padding: 10rpx 20rpx;
font-size: 26rpx;
color: #007aff;
background: #e8f4fd;
border-radius: 15rpx;
}
}
.vital-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.vital-item {
position: relative;
display: flex;
align-items: center;
padding: 25rpx;
background: #f8f9fa;
border-radius: 15rpx;
.vital-icon {
margin-right: 20rpx;
.icon {
font-size: 32rpx;
}
}
.vital-info {
flex: 1;
.vital-value {
margin-right: 5rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.vital-unit {
font-size: 20rpx;
color: #999;
}
.vital-label {
display: block;
margin-top: 5rpx;
font-size: 24rpx;
color: #666;
}
}
.vital-status {
position: absolute;
top: 10rpx;
right: 10rpx;
padding: 5rpx 10rpx;
font-size: 20rpx;
border-radius: 8rpx;
&.normal {
color: #34c759;
background: #e8f5e8;
}
&.warning {
color: #ff9500;
background: #fff2e8;
}
&.danger {
color: #ff3b30;
background: #ffe8e8;
}
}
}
}
.health-trends {
padding: 30rpx;
margin-bottom: 30rpx;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.section-title {
margin-bottom: 30rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.trend-chart {
.chart-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 200rpx;
margin-bottom: 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
.chart-text {
font-size: 32rpx;
color: #999;
}
}
.trend-summary {
display: flex;
justify-content: space-around;
.trend-item {
text-align: center;
.trend-label {
display: block;
margin-bottom: 10rpx;
font-size: 24rpx;
color: #666;
}
.trend-value {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
}
}
}
.health-reports {
padding: 30rpx;
margin-bottom: 30rpx;
background: white;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.more-link {
font-size: 26rpx;
color: #007aff;
}
}
.report-list {
.report-item {
display: flex;
align-items: center;
padding: 25rpx;
margin-bottom: 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
.report-icon {
margin-right: 20rpx;
.icon {
font-size: 32rpx;
}
}
.report-info {
flex: 1;
.report-title {
margin-bottom: 8rpx;
font-size: 28rpx;
color: #333;
}
.report-date {
font-size: 24rpx;
color: #666;
}
}
.report-score {
font-size: 32rpx;
font-weight: bold;
&.good {
color: #34c759;
}
&.normal {
color: #ff9500;
}
&.poor {
color: #ff3b30;
}
}
}
}
}
.generate-report {
padding: 30rpx 0;
.generate-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 25rpx;
background: linear-gradient(45deg, #007aff, #5ac8fa);
border: none;
border-radius: 50rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 122, 255, 0.3);
.btn-icon {
margin-right: 15rpx;
font-size: 32rpx;
}
.btn-text {
font-size: 30rpx;
font-weight: bold;
color: white;
}
}
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,454 @@
<template>
<view class="diet-management">
<!-- 今日营养摄入概览 -->
<view class="nutrition-summary">
<view class="summary-header">
<text class="date">{{ formatDate(currentDate) }}</text>
<text class="tips">营养摄入概览</text>
</view>
<view class="nutrition-circle">
<view class="circle-item">
<view
class="circle-progress"
:style="{
background: `conic-gradient(#4CAF50 0deg ${calorieProgress}deg, #f0f0f0 ${calorieProgress}deg 360deg)`,
}"
>
<view class="circle-content">
<text class="value">{{ todayNutrition.calories }}</text>
<text class="unit">kcal</text>
</view>
</view>
<text class="label">热量</text>
</view>
<view class="nutrients">
<view class="nutrient-item">
<text class="nutrient-name">蛋白质</text>
<text class="nutrient-value">{{ todayNutrition.protein }}g</text>
</view>
<view class="nutrient-item">
<text class="nutrient-name">脂肪</text>
<text class="nutrient-value">{{ todayNutrition.fat }}g</text>
</view>
<view class="nutrient-item">
<text class="nutrient-name">碳水</text>
<text class="nutrient-value">{{ todayNutrition.carbs }}g</text>
</view>
</view>
</view>
</view>
<!-- 中医体质膳食建议 -->
<view class="constitution-diet">
<view class="section-title">
<text>体质膳食建议</text>
<text class="constitution-type">{{ userConstitution }}</text>
</view>
<view class="diet-suggestions">
<view v-for="suggestion in dietSuggestions" :key="suggestion.id" class="suggestion-item">
<image :src="suggestion.image" class="suggestion-image" />
<view class="suggestion-content">
<text class="suggestion-name">{{ suggestion.name }}</text>
<text class="suggestion-desc">{{ suggestion.description }}</text>
<text class="suggestion-effect">{{ suggestion.effect }}</text>
</view>
</view>
</view>
</view>
<!-- 每日膳食记录 -->
<view class="daily-meals">
<view class="section-title">今日膳食</view>
<view class="meal-list">
<view v-for="meal in todayMeals" :key="meal.id" class="meal-item">
<view class="meal-header">
<text class="meal-time">{{ meal.time }}</text>
<text class="meal-calories">{{ meal.calories }}kcal</text>
</view>
<view class="meal-foods">
<text v-for="food in meal.foods" :key="food" class="food-item">{{ food }}</text>
</view>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<button class="action-btn primary" @click="goToRecord">记录饮食</button>
<button class="action-btn secondary" @click="viewHistory">查看历史</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
interface NutritionData {
calories: number;
protein: number;
fat: number;
carbs: number;
}
interface Meal {
id: string;
time: string;
calories: number;
foods: string[];
}
interface DietSuggestion {
id: string;
name: string;
description: string;
effect: string;
image: string;
}
const currentDate = ref(new Date());
const userConstitution = ref("气虚体质");
const todayNutrition = ref<NutritionData>({
calories: 1650,
protein: 85,
fat: 58,
carbs: 205,
});
const todayMeals = ref<Meal[]>([
{
id: "1",
time: "早餐 8:00",
calories: 420,
foods: ["小米粥", "鸡蛋", "包子", "咸菜"],
},
{
id: "2",
time: "午餐 12:30",
calories: 680,
foods: ["米饭", "红烧肉", "青菜", "冬瓜汤"],
},
{
id: "3",
time: "晚餐 18:00",
calories: 550,
foods: ["面条", "西红柿鸡蛋", "拌黄瓜"],
},
]);
const dietSuggestions = ref<DietSuggestion[]>([
{
id: "1",
name: "山药薏米粥",
description: "健脾益气,适合气虚体质",
effect: "补气健脾,增强体质",
image: "/static/images/diet/yam-porridge.jpg",
},
{
id: "2",
name: "黄芪炖鸡",
description: "补气养血,增强免疫力",
effect: "益气固表,补虚强身",
image: "/static/images/diet/chicken-soup.jpg",
},
{
id: "3",
name: "红枣银耳汤",
description: "滋阴润燥,养血安神",
effect: "补血养颜,润肺止咳",
image: "/static/images/diet/dates-soup.jpg",
},
]);
const calorieProgress = computed(() => {
const targetCalories = 2000;
return Math.min((todayNutrition.value.calories / targetCalories) * 360, 360);
});
const formatDate = (date: Date) => {
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}${day}`;
};
const goToRecord = () => {
uni.navigateTo({
url: "/pages/health/diet/record",
});
};
const viewHistory = () => {
uni.navigateTo({
url: "/pages/health/diet/history",
});
};
onMounted(() => {
// 获取用户体质信息和膳食数据
});
</script>
<style lang="scss" scoped>
.diet-management {
min-height: 100vh;
padding: 20rpx;
background-color: #f8f9fa;
}
.nutrition-summary {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.summary-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.date {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.tips {
font-size: 24rpx;
color: #666;
}
}
.nutrition-circle {
display: flex;
align-items: center;
justify-content: space-between;
}
.circle-item {
display: flex;
flex-direction: column;
align-items: center;
}
.circle-progress {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
&::before {
position: absolute;
width: 120rpx;
height: 120rpx;
content: "";
background: white;
border-radius: 50%;
}
}
.circle-content {
position: relative;
z-index: 1;
text-align: center;
.value {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.unit {
font-size: 20rpx;
color: #666;
}
}
.label {
margin-top: 16rpx;
font-size: 24rpx;
color: #666;
}
.nutrients {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.nutrient-item {
display: flex;
align-items: center;
justify-content: space-between;
min-width: 200rpx;
padding: 16rpx 20rpx;
background: #f8f9fa;
border-radius: 8rpx;
.nutrient-name {
font-size: 24rpx;
color: #666;
}
.nutrient-value {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
}
.constitution-diet {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
.constitution-type {
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #4caf50;
background: #e8f5e8;
border-radius: 20rpx;
}
}
.diet-suggestions {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.suggestion-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
.suggestion-image {
width: 100rpx;
height: 100rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.suggestion-content {
flex: 1;
.suggestion-name {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.suggestion-desc {
display: block;
margin-bottom: 8rpx;
font-size: 22rpx;
color: #666;
}
.suggestion-effect {
display: block;
font-size: 20rpx;
color: #4caf50;
}
}
}
.daily-meals {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.meal-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.meal-item {
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
.meal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
.meal-time {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.meal-calories {
font-size: 24rpx;
color: #4caf50;
}
}
.meal-foods {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.food-item {
padding: 6rpx 12rpx;
font-size: 22rpx;
color: #666;
background: white;
border-radius: 16rpx;
}
}
}
.bottom-actions {
display: flex;
gap: 20rpx;
padding: 30rpx 0;
.action-btn {
flex: 1;
height: 88rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
&.primary {
color: white;
background: #4caf50;
}
&.secondary {
color: #666;
background: #f0f0f0;
}
}
}
</style>

View File

@ -0,0 +1,505 @@
<template>
<view class="diet-record">
<!-- 选择餐次 -->
<view class="meal-type-selector">
<view
v-for="type in mealTypes"
:key="type.value"
class="meal-type-item"
:class="{ active: selectedMealType === type.value }"
@click="selectMealType(type.value)"
>
<text>{{ type.label }}</text>
</view>
</view>
<!-- 搜索食物 -->
<view class="search-section">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input v-model="searchKeyword" placeholder="搜索食物" @input="searchFood" />
</view>
</view>
<!-- 常用食物 -->
<view v-if="!searchKeyword" class="common-foods">
<view class="section-title">常用食物</view>
<view class="food-grid">
<view
v-for="food in commonFoods"
:key="food.id"
class="food-item"
@click="selectFood(food)"
>
<image :src="food.image" class="food-image" />
<text class="food-name">{{ food.name }}</text>
<text class="food-calories">{{ food.calories }}kcal/100g</text>
</view>
</view>
</view>
<!-- 搜索结果 -->
<view v-if="searchKeyword" class="search-results">
<view class="section-title">搜索结果</view>
<view class="food-list">
<view
v-for="food in searchResults"
:key="food.id"
class="food-item"
@click="selectFood(food)"
>
<image :src="food.image" class="food-image" />
<view class="food-info">
<text class="food-name">{{ food.name }}</text>
<text class="food-nutrition">热量: {{ food.calories }}kcal/100g</text>
</view>
</view>
</view>
</view>
<!-- 已选择的食物 -->
<view v-if="selectedFoods.length > 0" class="selected-foods">
<view class="section-title">已选择食物</view>
<view class="selected-list">
<view v-for="(food, index) in selectedFoods" :key="index" class="selected-item">
<view class="food-info">
<text class="food-name">{{ food.name }}</text>
<text class="food-amount">{{ food.amount }}g</text>
</view>
<view class="amount-controls">
<button class="control-btn" @click="adjustAmount(index, -10)">-</button>
<input v-model.number="food.amount" type="number" class="amount-input" />
<button class="control-btn" @click="adjustAmount(index, 10)">+</button>
</view>
<button class="remove-btn" @click="removeFood(index)">删除</button>
</view>
</view>
<!-- 营养汇总 -->
<view class="nutrition-summary">
<text class="summary-title">营养汇总</text>
<view class="nutrition-row">
<text>总热量: {{ totalNutrition.calories }}kcal</text>
<text>蛋白质: {{ totalNutrition.protein }}g</text>
</view>
<view class="nutrition-row">
<text>脂肪: {{ totalNutrition.fat }}g</text>
<text>碳水: {{ totalNutrition.carbs }}g</text>
</view>
</view>
</view>
<!-- 底部保存按钮 -->
<view v-if="selectedFoods.length > 0" class="bottom-actions">
<button class="save-btn" @click="saveDietRecord">保存记录</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
interface Food {
id: string;
name: string;
calories: number;
protein: number;
fat: number;
carbs: number;
image: string;
}
interface SelectedFood extends Food {
amount: number;
}
interface MealType {
value: string;
label: string;
}
const mealTypes: MealType[] = [
{ value: "breakfast", label: "早餐" },
{ value: "lunch", label: "午餐" },
{ value: "dinner", label: "晚餐" },
{ value: "snack", label: "加餐" },
];
const selectedMealType = ref("breakfast");
const searchKeyword = ref("");
const selectedFoods = ref<SelectedFood[]>([]);
const commonFoods = ref<Food[]>([
{
id: "1",
name: "米饭",
calories: 116,
protein: 2.6,
fat: 0.3,
carbs: 25.9,
image: "/static/images/food/rice.jpg",
},
{
id: "2",
name: "鸡蛋",
calories: 144,
protein: 13.3,
fat: 8.8,
carbs: 2.8,
image: "/static/images/food/egg.jpg",
},
{
id: "3",
name: "鸡胸肉",
calories: 133,
protein: 19.4,
fat: 5.0,
carbs: 2.5,
image: "/static/images/food/chicken.jpg",
},
{
id: "4",
name: "西兰花",
calories: 34,
protein: 4.1,
fat: 0.6,
carbs: 4.3,
image: "/static/images/food/broccoli.jpg",
},
{
id: "5",
name: "苹果",
calories: 52,
protein: 0.2,
fat: 0.2,
carbs: 13.8,
image: "/static/images/food/apple.jpg",
},
{
id: "6",
name: "牛奶",
calories: 54,
protein: 3.0,
fat: 3.2,
carbs: 3.4,
image: "/static/images/food/milk.jpg",
},
]);
const searchResults = ref<Food[]>([]);
const totalNutrition = computed(() => {
return selectedFoods.value.reduce(
(total, food) => {
const ratio = food.amount / 100;
return {
calories: Math.round(total.calories + food.calories * ratio),
protein: Math.round((total.protein + food.protein * ratio) * 10) / 10,
fat: Math.round((total.fat + food.fat * ratio) * 10) / 10,
carbs: Math.round((total.carbs + food.carbs * ratio) * 10) / 10,
};
},
{ calories: 0, protein: 0, fat: 0, carbs: 0 }
);
});
const selectMealType = (type: string) => {
selectedMealType.value = type;
};
const searchFood = () => {
if (!searchKeyword.value) {
searchResults.value = [];
return;
}
// 模拟搜索结果
searchResults.value = commonFoods.value.filter((food) => food.name.includes(searchKeyword.value));
};
const selectFood = (food: Food) => {
const selectedFood: SelectedFood = {
...food,
amount: 100,
};
selectedFoods.value.push(selectedFood);
};
const adjustAmount = (index: number, delta: number) => {
const newAmount = selectedFoods.value[index].amount + delta;
if (newAmount > 0) {
selectedFoods.value[index].amount = newAmount;
}
};
const removeFood = (index: number) => {
selectedFoods.value.splice(index, 1);
};
const saveDietRecord = () => {
// 保存饮食记录
uni.showToast({
title: "记录保存成功",
icon: "success",
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
};
</script>
<style lang="scss" scoped>
.diet-record {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 120rpx;
background-color: #f8f9fa;
}
.meal-type-selector {
display: flex;
padding: 8rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.meal-type-item {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 68rpx;
font-size: 26rpx;
color: #666;
border-radius: 12rpx;
transition: all 0.3s;
&.active {
color: white;
background: #4caf50;
}
}
.search-section {
margin-bottom: 20rpx;
}
.search-bar {
display: flex;
align-items: center;
padding: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
input {
flex: 1;
margin-left: 20rpx;
font-size: 26rpx;
}
}
.section-title {
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.common-foods {
margin-bottom: 20rpx;
}
.food-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.food-item {
padding: 20rpx;
text-align: center;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.food-image {
width: 80rpx;
height: 80rpx;
margin-bottom: 12rpx;
border-radius: 8rpx;
}
.food-name {
display: block;
margin-bottom: 8rpx;
font-size: 24rpx;
color: #333;
}
.food-calories {
font-size: 20rpx;
color: #666;
}
}
.search-results {
margin-bottom: 20rpx;
}
.food-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.food-list .food-item {
display: flex;
align-items: center;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.food-image {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.food-info {
flex: 1;
.food-name {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
color: #333;
}
.food-nutrition {
font-size: 22rpx;
color: #666;
}
}
}
.selected-foods {
margin-bottom: 20rpx;
}
.selected-list {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 20rpx;
}
.selected-item {
display: flex;
align-items: center;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.food-info {
flex: 1;
.food-name {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
color: #333;
}
.food-amount {
font-size: 22rpx;
color: #666;
}
}
.amount-controls {
display: flex;
align-items: center;
margin-right: 20rpx;
.control-btn {
width: 60rpx;
height: 60rpx;
font-size: 28rpx;
color: #666;
background: #f0f0f0;
border: none;
border-radius: 50%;
}
.amount-input {
width: 80rpx;
height: 60rpx;
margin: 0 12rpx;
font-size: 24rpx;
text-align: center;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
}
}
.remove-btn {
padding: 12rpx 20rpx;
font-size: 22rpx;
color: white;
background: #ff4444;
border: none;
border-radius: 8rpx;
}
}
.nutrition-summary {
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.summary-title {
display: block;
margin-bottom: 16rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.nutrition-row {
display: flex;
justify-content: space-between;
margin-bottom: 12rpx;
font-size: 24rpx;
color: #666;
}
}
.bottom-actions {
position: fixed;
right: 0;
bottom: 0;
left: 0;
padding: 20rpx;
background: white;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.save-btn {
width: 100%;
height: 88rpx;
font-size: 28rpx;
font-weight: 600;
color: white;
background: #4caf50;
border: none;
border-radius: 44rpx;
}
</style>

View File

@ -0,0 +1,945 @@
<template>
<view class="health-education">
<!-- 轮播图 -->
<view class="banner-section">
<swiper class="banner-swiper" indicator-dots circular autoplay>
<swiper-item v-for="banner in banners" :key="banner.id">
<view class="banner-item" @click="viewContent(banner)">
<image :src="banner.image" class="banner-image" />
<view class="banner-overlay">
<text class="banner-title">{{ banner.title }}</text>
<text class="banner-desc">{{ banner.description }}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
<!-- 分类标签 -->
<view class="category-tabs">
<scroll-view scroll-x="true" class="tabs-scroll">
<view class="tabs-list">
<view
v-for="tab in educationTabs"
:key="tab.id"
class="tab-item"
:class="{ active: activeTab === tab.id }"
@click="switchTab(tab.id)"
>
<text>{{ tab.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 内容列表 -->
<view class="content-section">
<!-- 专家讲座 -->
<view v-if="activeTab === 'expert'" class="expert-lectures">
<view class="section-header">
<text class="section-title">专家讲座</text>
<text class="section-more" @click="viewMore('expert')">更多</text>
</view>
<view class="lecture-list">
<view
v-for="lecture in expertLectures"
:key="lecture.id"
class="lecture-item"
@click="playVideo(lecture)"
>
<view class="lecture-video">
<image :src="lecture.cover" class="video-cover" />
<view class="play-button">
<uni-icons type="play-filled" size="24" color="#fff"></uni-icons>
</view>
<text class="video-duration">{{ lecture.duration }}</text>
</view>
<view class="lecture-info">
<text class="lecture-title">{{ lecture.title }}</text>
<text class="lecture-expert">{{ lecture.expert }}</text>
<view class="lecture-meta">
<text class="lecture-views">{{ lecture.views }}次观看</text>
<text class="lecture-date">{{ formatDate(lecture.publishDate) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 健康资讯 -->
<view v-if="activeTab === 'news'" class="health-news">
<view class="section-header">
<text class="section-title">健康资讯</text>
<text class="section-more" @click="viewMore('news')">更多</text>
</view>
<view class="news-list">
<view
v-for="news in healthNews"
:key="news.id"
class="news-item"
@click="readArticle(news)"
>
<image :src="news.image" class="news-image" />
<view class="news-content">
<text class="news-title">{{ news.title }}</text>
<text class="news-summary">{{ news.summary }}</text>
<view class="news-meta">
<text class="news-source">{{ news.source }}</text>
<text class="news-time">{{ formatTime(news.publishTime) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 养生知识 -->
<view v-if="activeTab === 'wellness'" class="wellness-knowledge">
<view class="section-header">
<text class="section-title">养生知识</text>
<text class="section-more" @click="viewMore('wellness')">更多</text>
</view>
<view class="knowledge-grid">
<view
v-for="knowledge in wellnessKnowledge"
:key="knowledge.id"
class="knowledge-card"
@click="viewKnowledge(knowledge)"
>
<image :src="knowledge.image" class="knowledge-image" />
<view class="knowledge-info">
<text class="knowledge-title">{{ knowledge.title }}</text>
<text class="knowledge-tag">{{ knowledge.category }}</text>
</view>
</view>
</view>
</view>
<!-- 食疗配方 -->
<view v-if="activeTab === 'diet'" class="diet-recipes">
<view class="section-header">
<text class="section-title">食疗配方</text>
<text class="section-more" @click="viewMore('diet')">更多</text>
</view>
<view class="recipe-list">
<view
v-for="recipe in dietRecipes"
:key="recipe.id"
class="recipe-item"
@click="viewRecipe(recipe)"
>
<image :src="recipe.image" class="recipe-image" />
<view class="recipe-content">
<text class="recipe-title">{{ recipe.title }}</text>
<text class="recipe-effect">{{ recipe.effect }}</text>
<view class="recipe-ingredients">
<text
v-for="ingredient in recipe.ingredients.slice(0, 3)"
:key="ingredient"
class="ingredient"
>
{{ ingredient }}
</text>
<text v-if="recipe.ingredients.length > 3" class="more-ingredients">
{{ recipe.ingredients.length }}
</text>
</view>
<view class="recipe-meta">
<text class="recipe-difficulty">{{ recipe.difficulty }}</text>
<text class="recipe-time">{{ recipe.cookTime }}分钟</text>
</view>
</view>
</view>
</view>
</view>
<!-- 运动指导 -->
<view v-if="activeTab === 'exercise'" class="exercise-guide">
<view class="section-header">
<text class="section-title">运动指导</text>
<text class="section-more" @click="viewMore('exercise')">更多</text>
</view>
<view class="guide-list">
<view
v-for="guide in exerciseGuides"
:key="guide.id"
class="guide-item"
@click="watchGuide(guide)"
>
<view class="guide-video">
<image :src="guide.cover" class="guide-cover" />
<view class="play-icon">
<uni-icons type="play-filled" size="20" color="#fff"></uni-icons>
</view>
</view>
<view class="guide-info">
<text class="guide-title">{{ guide.title }}</text>
<text class="guide-instructor">{{ guide.instructor }}</text>
<view class="guide-tags">
<text v-for="tag in guide.tags" :key="tag" class="guide-tag">{{ tag }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
interface Banner {
id: string;
title: string;
description: string;
image: string;
type: string;
targetId: string;
}
interface ExpertLecture {
id: string;
title: string;
expert: string;
cover: string;
duration: string;
views: number;
publishDate: Date;
videoUrl: string;
}
interface HealthNews {
id: string;
title: string;
summary: string;
image: string;
source: string;
publishTime: Date;
content: string;
}
interface WellnessKnowledge {
id: string;
title: string;
image: string;
category: string;
content: string;
}
interface DietRecipe {
id: string;
title: string;
effect: string;
image: string;
ingredients: string[];
difficulty: string;
cookTime: number;
steps: string[];
}
interface ExerciseGuide {
id: string;
title: string;
instructor: string;
cover: string;
tags: string[];
videoUrl: string;
}
const activeTab = ref("expert");
const educationTabs = [
{ id: "expert", name: "专家讲座" },
{ id: "news", name: "健康资讯" },
{ id: "wellness", name: "养生知识" },
{ id: "diet", name: "食疗配方" },
{ id: "exercise", name: "运动指导" },
];
const banners = ref<Banner[]>([
{
id: "1",
title: "春季养生要点",
description: "著名中医专家详解春季如何调理身体",
image: "/static/images/banner/spring-health.jpg",
type: "video",
targetId: "lecture1",
},
{
id: "2",
title: "中医体质辨识指南",
description: "了解自己的体质,科学调理身体",
image: "/static/images/banner/constitution.jpg",
type: "article",
targetId: "article1",
},
{
id: "3",
title: "食疗养生经典方",
description: "传统食疗配方,日常保健必备",
image: "/static/images/banner/diet-therapy.jpg",
type: "recipe",
targetId: "recipe1",
},
]);
const expertLectures = ref<ExpertLecture[]>([
{
id: "1",
title: "春季养肝护肝的中医方法",
expert: "李教授 - 中医内科专家",
cover: "/static/images/lectures/liver-care.jpg",
duration: "25:30",
views: 12500,
publishDate: new Date("2024-06-20"),
videoUrl: "https://example.com/video1.mp4",
},
{
id: "2",
title: "气虚体质的调理要点",
expert: "王医生 - 中医体质专家",
cover: "/static/images/lectures/qixu-care.jpg",
duration: "18:45",
views: 8900,
publishDate: new Date("2024-06-18"),
videoUrl: "https://example.com/video2.mp4",
},
{
id: "3",
title: "中医穴位按摩入门",
expert: "张医师 - 针灸推拿专家",
cover: "/static/images/lectures/acupoint-massage.jpg",
duration: "32:15",
views: 15600,
publishDate: new Date("2024-06-15"),
videoUrl: "https://example.com/video3.mp4",
},
]);
const healthNews = ref<HealthNews[]>([
{
id: "1",
title: "世界卫生组织发布最新中医药发展报告",
summary: "报告显示中医药在全球范围内的认知度和应用率持续提升",
image: "/static/images/news/who-report.jpg",
source: "健康时报",
publishTime: new Date("2024-06-22"),
content: "详细新闻内容...",
},
{
id: "2",
title: "夏季防暑降温的中医智慧",
summary: "专家建议通过饮食调理和穴位按摩来预防中暑",
image: "/static/images/news/summer-heat.jpg",
source: "中医药报",
publishTime: new Date("2024-06-21"),
content: "详细新闻内容...",
},
{
id: "3",
title: "中医治疗失眠症状有新突破",
summary: "最新研究表明针灸结合中药治疗失眠效果显著",
image: "/static/images/news/insomnia-treatment.jpg",
source: "医学前沿",
publishTime: new Date("2024-06-20"),
content: "详细新闻内容...",
},
]);
const wellnessKnowledge = ref<WellnessKnowledge[]>([
{
id: "1",
title: "四季养生原则",
image: "/static/images/wellness/four-seasons.jpg",
category: "基础理论",
content: "四季养生详细内容...",
},
{
id: "2",
title: "睡眠养生法",
image: "/static/images/wellness/sleep-health.jpg",
category: "生活养生",
content: "睡眠养生详细内容...",
},
{
id: "3",
title: "情志调养",
image: "/static/images/wellness/emotion-care.jpg",
category: "心理健康",
content: "情志调养详细内容...",
},
{
id: "4",
title: "经络养生",
image: "/static/images/wellness/meridian.jpg",
category: "传统疗法",
content: "经络养生详细内容...",
},
]);
const dietRecipes = ref<DietRecipe[]>([
{
id: "1",
title: "银耳莲子汤",
effect: "滋阴润燥,养心安神",
image: "/static/images/recipes/yiner-lotus.jpg",
ingredients: ["银耳", "莲子", "冰糖", "红枣"],
difficulty: "简单",
cookTime: 45,
steps: ["银耳泡发...", "莲子去芯...", "煮制过程..."],
},
{
id: "2",
title: "黄芪当归炖鸡",
effect: "补气养血,增强免疫",
image: "/static/images/recipes/huangqi-chicken.jpg",
ingredients: ["黄芪", "当归", "土鸡", "生姜", "枸杞"],
difficulty: "中等",
cookTime: 90,
steps: ["鸡肉处理...", "药材准备...", "炖制过程..."],
},
{
id: "3",
title: "山药薏米粥",
effect: "健脾益气,祛湿止泻",
image: "/static/images/recipes/yam-barley.jpg",
ingredients: ["山药", "薏米", "大米", "红豆"],
difficulty: "简单",
cookTime: 60,
steps: ["薏米浸泡...", "山药处理...", "煮粥过程..."],
},
]);
const exerciseGuides = ref<ExerciseGuide[]>([
{
id: "1",
title: "太极拳入门教学",
instructor: "陈师傅",
cover: "/static/images/guides/taichi-basic.jpg",
tags: ["太极拳", "入门", "传统运动"],
videoUrl: "https://example.com/taichi1.mp4",
},
{
id: "2",
title: "八段锦完整教程",
instructor: "李教练",
cover: "/static/images/guides/baduanjin.jpg",
tags: ["八段锦", "养生功法", "完整版"],
videoUrl: "https://example.com/baduanjin.mp4",
},
{
id: "3",
title: "五禽戏健身法",
instructor: "王老师",
cover: "/static/images/guides/wuqinxi.jpg",
tags: ["五禽戏", "健身", "模仿动物"],
videoUrl: "https://example.com/wuqinxi.mp4",
},
]);
const switchTab = (tabId: string) => {
activeTab.value = tabId;
};
const formatDate = (date: Date) => {
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}${day}`;
};
const formatTime = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return "今天";
if (days === 1) return "昨天";
if (days < 7) return `${days}天前`;
return formatDate(date);
};
const viewContent = (banner: Banner) => {
if (banner.type === "video") {
playVideo({ id: banner.targetId } as ExpertLecture);
} else if (banner.type === "article") {
readArticle({ id: banner.targetId } as HealthNews);
}
};
const playVideo = (lecture: ExpertLecture) => {
uni.navigateTo({
url: `/pages/health/education/video?id=${lecture.id}`,
});
};
const readArticle = (news: HealthNews) => {
uni.navigateTo({
url: `/pages/health/education/article?id=${news.id}`,
});
};
const viewKnowledge = (knowledge: WellnessKnowledge) => {
uni.navigateTo({
url: `/pages/health/education/knowledge?id=${knowledge.id}`,
});
};
const viewRecipe = (recipe: DietRecipe) => {
uni.navigateTo({
url: `/pages/health/education/recipe?id=${recipe.id}`,
});
};
const watchGuide = (guide: ExerciseGuide) => {
uni.navigateTo({
url: `/pages/health/education/guide?id=${guide.id}`,
});
};
const viewMore = (type: string) => {
uni.navigateTo({
url: `/pages/health/education/list?type=${type}`,
});
};
</script>
<style lang="scss" scoped>
.health-education {
min-height: 100vh;
background-color: #f8f9fa;
}
.banner-section {
height: 400rpx;
margin-bottom: 20rpx;
}
.banner-swiper {
height: 100%;
}
.banner-item {
position: relative;
height: 100%;
}
.banner-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.banner-overlay {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 40rpx 30rpx 30rpx;
color: white;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
.banner-title {
display: block;
margin-bottom: 12rpx;
font-size: 32rpx;
font-weight: 600;
line-height: 1.3;
}
.banner-desc {
font-size: 24rpx;
line-height: 1.4;
opacity: 0.9;
}
}
.category-tabs {
padding: 20rpx 0;
background: white;
border-bottom: 1rpx solid #f0f0f0;
}
.tabs-scroll {
width: 100%;
}
.tabs-list {
display: flex;
gap: 32rpx;
padding: 0 20rpx;
}
.tab-item {
padding-bottom: 16rpx;
font-size: 28rpx;
color: #666;
white-space: nowrap;
border-bottom: 3rpx solid transparent;
transition: all 0.3s;
&.active {
font-weight: 600;
color: #2196f3;
border-bottom-color: #2196f3;
}
}
.content-section {
padding: 20rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.section-more {
font-size: 24rpx;
color: #2196f3;
}
}
// 专家讲座样式
.lecture-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.lecture-item {
display: flex;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.lecture-video {
position: relative;
width: 200rpx;
height: 120rpx;
margin-right: 20rpx;
overflow: hidden;
border-radius: 8rpx;
.video-cover {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-button {
position: absolute;
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
background: rgba(0, 0, 0, 0.7);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.video-duration {
position: absolute;
right: 8rpx;
bottom: 8rpx;
padding: 4rpx 8rpx;
font-size: 20rpx;
color: white;
background: rgba(0, 0, 0, 0.7);
border-radius: 4rpx;
}
}
.lecture-info {
flex: 1;
.lecture-title {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
line-height: 1.3;
color: #333;
}
.lecture-expert {
display: block;
margin-bottom: 12rpx;
font-size: 22rpx;
color: #2196f3;
}
.lecture-meta {
display: flex;
justify-content: space-between;
.lecture-views,
.lecture-date {
font-size: 20rpx;
color: #999;
}
}
}
// 健康资讯样式
.news-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.news-item {
display: flex;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.news-image {
width: 120rpx;
height: 120rpx;
margin-right: 20rpx;
object-fit: cover;
border-radius: 8rpx;
}
.news-content {
flex: 1;
.news-title {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
line-height: 1.3;
color: #333;
}
.news-summary {
display: block;
display: -webkit-box;
margin-bottom: 12rpx;
overflow: hidden;
font-size: 22rpx;
line-height: 1.4;
color: #666;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.news-meta {
display: flex;
justify-content: space-between;
.news-source {
font-size: 20rpx;
color: #2196f3;
}
.news-time {
font-size: 20rpx;
color: #999;
}
}
}
}
// 养生知识样式
.knowledge-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.knowledge-card {
overflow: hidden;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.knowledge-image {
width: 100%;
height: 200rpx;
object-fit: cover;
}
.knowledge-info {
padding: 20rpx;
.knowledge-title {
display: block;
margin-bottom: 8rpx;
font-size: 24rpx;
font-weight: 600;
line-height: 1.3;
color: #333;
}
.knowledge-tag {
padding: 4rpx 12rpx;
font-size: 20rpx;
color: #2196f3;
background: #e3f2fd;
border-radius: 12rpx;
}
}
}
// 食疗配方样式
.recipe-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.recipe-item {
display: flex;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.recipe-image {
width: 120rpx;
height: 120rpx;
margin-right: 20rpx;
object-fit: cover;
border-radius: 8rpx;
}
.recipe-content {
flex: 1;
.recipe-title {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.recipe-effect {
display: block;
margin-bottom: 12rpx;
font-size: 22rpx;
color: #4caf50;
}
.recipe-ingredients {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 12rpx;
.ingredient,
.more-ingredients {
padding: 4rpx 8rpx;
font-size: 20rpx;
color: #666;
background: #f0f0f0;
border-radius: 8rpx;
}
}
.recipe-meta {
display: flex;
gap: 20rpx;
.recipe-difficulty,
.recipe-time {
font-size: 20rpx;
color: #999;
}
}
}
}
// 运动指导样式
.guide-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.guide-item {
display: flex;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.guide-video {
position: relative;
width: 120rpx;
height: 120rpx;
margin-right: 20rpx;
overflow: hidden;
border-radius: 8rpx;
.guide-cover {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.7);
border-radius: 50%;
transform: translate(-50%, -50%);
}
}
.guide-info {
flex: 1;
.guide-title {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.guide-instructor {
display: block;
margin-bottom: 12rpx;
font-size: 22rpx;
color: #2196f3;
}
.guide-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
.guide-tag {
padding: 4rpx 8rpx;
font-size: 20rpx;
color: #4caf50;
background: #e8f5e8;
border-radius: 8rpx;
}
}
}
}
</style>

View File

@ -0,0 +1,497 @@
<template>
<view class="encyclopedia">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input v-model="searchKeyword" placeholder="搜索中医知识..." @input="searchContent" />
</view>
</view>
<!-- 分类导航 -->
<view class="category-nav">
<scroll-view scroll-x="true" class="category-scroll">
<view class="category-list">
<view
v-for="category in categories"
:key="category.id"
class="category-item"
:class="{ active: activeCategory === category.id }"
@click="switchCategory(category.id)"
>
<view class="category-icon" :style="{ backgroundColor: category.color }">
<text>{{ category.name.charAt(0) }}</text>
</view>
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 内容区域 -->
<view class="content-area">
<!-- 搜索结果 -->
<view v-if="searchKeyword && searchResults.length > 0" class="search-results">
<view class="section-title">搜索结果</view>
<view class="content-list">
<view
v-for="item in searchResults"
:key="item.id"
class="content-item"
@click="viewDetail(item)"
>
<image :src="item.image" class="content-image" />
<view class="content-info">
<text class="content-title">{{ item.title }}</text>
<text class="content-desc">{{ item.description }}</text>
<view class="content-meta">
<text class="content-category">{{ item.categoryName }}</text>
<text class="content-views">{{ item.views }}次阅读</text>
</view>
</view>
</view>
</view>
</view>
<!-- 分类内容 -->
<view v-else class="category-content">
<!-- 热门推荐 -->
<view v-if="activeCategory === 'all'" class="hot-section">
<view class="section-title">热门推荐</view>
<view class="hot-grid">
<view
v-for="item in hotItems"
:key="item.id"
class="hot-item"
@click="viewDetail(item)"
>
<image :src="item.image" class="hot-image" />
<view class="hot-overlay">
<text class="hot-title">{{ item.title }}</text>
<text class="hot-views">{{ item.views }}次阅读</text>
</view>
</view>
</view>
</view>
<!-- 分类列表 -->
<view class="category-section">
<view class="section-title">{{ getCurrentCategoryName() }}</view>
<view class="content-list">
<view
v-for="item in currentContent"
:key="item.id"
class="content-item"
@click="viewDetail(item)"
>
<image :src="item.image" class="content-image" />
<view class="content-info">
<text class="content-title">{{ item.title }}</text>
<text class="content-desc">{{ item.description }}</text>
<view class="content-meta">
<text class="content-date">{{ formatDate(item.publishDate) }}</text>
<text class="content-views">{{ item.views }}次阅读</text>
</view>
</view>
<view class="content-action">
<uni-icons type="right" size="14" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
interface Category {
id: string;
name: string;
color: string;
}
interface EncyclopediaItem {
id: string;
title: string;
description: string;
image: string;
categoryId: string;
categoryName: string;
publishDate: Date;
views: number;
content: string;
}
const searchKeyword = ref("");
const activeCategory = ref("all");
const searchResults = ref<EncyclopediaItem[]>([]);
const categories = ref<Category[]>([
{ id: "all", name: "全部", color: "#2196F3" },
{ id: "herbs", name: "中药材", color: "#4CAF50" },
{ id: "acupoints", name: "穴位", color: "#FF9800" },
{ id: "constitution", name: "体质", color: "#9C27B0" },
{ id: "diet", name: "食疗", color: "#E91E63" },
{ id: "exercise", name: "养生", color: "#00BCD4" },
{ id: "theory", name: "理论", color: "#795548" },
]);
const encyclopediaData = ref<EncyclopediaItem[]>([
{
id: "1",
title: "人参的功效与作用",
description: "人参是名贵中药材,具有大补元气、复脉固脱、补脾益肺等功效",
image: "/static/images/herbs/ginseng.jpg",
categoryId: "herbs",
categoryName: "中药材",
publishDate: new Date("2024-06-20"),
views: 1250,
content: "人参详细介绍...",
},
{
id: "2",
title: "足三里穴位详解",
description: "足三里是人体重要穴位,具有调理脾胃、补中益气的作用",
image: "/static/images/acupoints/zusanli.jpg",
categoryId: "acupoints",
categoryName: "穴位",
publishDate: new Date("2024-06-19"),
views: 980,
content: "足三里穴位详细介绍...",
},
{
id: "3",
title: "气虚体质的调理方法",
description: "气虚体质表现为容易疲劳、气短懒言,需要通过饮食和运动来调理",
image: "/static/images/constitution/qixu.jpg",
categoryId: "constitution",
categoryName: "体质",
publishDate: new Date("2024-06-18"),
views: 1580,
content: "气虚体质调理详细方法...",
},
{
id: "4",
title: "山药薏米粥的制作与功效",
description: "山药薏米粥具有健脾益气、祛湿止泻的功效,适合脾虚湿重者食用",
image: "/static/images/diet/yam-porridge.jpg",
categoryId: "diet",
categoryName: "食疗",
publishDate: new Date("2024-06-17"),
views: 2100,
content: "山药薏米粥制作方法与功效详解...",
},
{
id: "5",
title: "太极拳的养生原理",
description: "太极拳动作柔和缓慢,能够调节气血,增强体质,是很好的养生运动",
image: "/static/images/exercise/taichi-theory.jpg",
categoryId: "exercise",
categoryName: "养生",
publishDate: new Date("2024-06-16"),
views: 875,
content: "太极拳养生原理详细解析...",
},
{
id: "6",
title: "中医五行学说",
description: "五行学说是中医理论的重要组成部分,用于解释人体生理病理现象",
image: "/static/images/theory/wuxing.jpg",
categoryId: "theory",
categoryName: "理论",
publishDate: new Date("2024-06-15"),
views: 650,
content: "五行学说详细介绍...",
},
{
id: "7",
title: "黄芪的药用价值",
description: "黄芪是常用补气药材,具有补气固表、利尿托毒等功效",
image: "/static/images/herbs/huangqi.jpg",
categoryId: "herbs",
categoryName: "中药材",
publishDate: new Date("2024-06-14"),
views: 1100,
content: "黄芪药用价值详解...",
},
{
id: "8",
title: "合谷穴的按摩方法",
description: "合谷穴是手阳明大肠经穴位,有清热解表、镇静止痛的作用",
image: "/static/images/acupoints/hegu.jpg",
categoryId: "acupoints",
categoryName: "穴位",
publishDate: new Date("2024-06-13"),
views: 750,
content: "合谷穴按摩方法与功效...",
},
]);
const hotItems = computed(() => {
return encyclopediaData.value.sort((a, b) => b.views - a.views).slice(0, 4);
});
const currentContent = computed(() => {
if (activeCategory.value === "all") {
return encyclopediaData.value;
}
return encyclopediaData.value.filter((item) => item.categoryId === activeCategory.value);
});
const getCurrentCategoryName = () => {
const category = categories.value.find((cat) => cat.id === activeCategory.value);
return category?.name || "全部";
};
const switchCategory = (categoryId: string) => {
activeCategory.value = categoryId;
searchKeyword.value = "";
searchResults.value = [];
};
const searchContent = () => {
if (!searchKeyword.value.trim()) {
searchResults.value = [];
return;
}
const keyword = searchKeyword.value.toLowerCase();
searchResults.value = encyclopediaData.value.filter(
(item) =>
item.title.toLowerCase().includes(keyword) || item.description.toLowerCase().includes(keyword)
);
};
const formatDate = (date: Date) => {
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}${day}`;
};
const viewDetail = (item: EncyclopediaItem) => {
uni.navigateTo({
url: `/pages/health/encyclopedia/detail?id=${item.id}`,
});
};
</script>
<style lang="scss" scoped>
.encyclopedia {
min-height: 100vh;
background-color: #f8f9fa;
}
.search-section {
padding: 20rpx;
background: white;
border-bottom: 1rpx solid #f0f0f0;
}
.search-bar {
display: flex;
align-items: center;
height: 72rpx;
padding: 0 24rpx;
background: #f8f9fa;
border-radius: 24rpx;
input {
flex: 1;
margin-left: 16rpx;
font-size: 26rpx;
color: #333;
}
}
.category-nav {
padding: 20rpx 0;
background: white;
border-bottom: 1rpx solid #f0f0f0;
}
.category-scroll {
width: 100%;
}
.category-list {
display: flex;
gap: 24rpx;
padding: 0 20rpx;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 100rpx;
&.active .category-icon {
transform: scale(1.1);
}
&.active .category-name {
font-weight: 600;
color: #2196f3;
}
.category-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
margin-bottom: 8rpx;
font-size: 24rpx;
font-weight: 600;
color: white;
border-radius: 50%;
transition: transform 0.3s;
}
.category-name {
font-size: 22rpx;
color: #666;
transition: all 0.3s;
}
}
.content-area {
padding: 20rpx;
}
.section-title {
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.hot-section {
margin-bottom: 30rpx;
}
.hot-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.hot-item {
position: relative;
height: 200rpx;
overflow: hidden;
border-radius: 12rpx;
.hot-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.hot-overlay {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 20rpx;
color: white;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
.hot-title {
display: block;
margin-bottom: 8rpx;
font-size: 24rpx;
font-weight: 600;
line-height: 1.3;
}
.hot-views {
font-size: 20rpx;
opacity: 0.8;
}
}
}
.content-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.content-item {
display: flex;
align-items: center;
padding: 20rpx;
background: white;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s;
&:active {
transform: scale(0.98);
}
.content-image {
width: 120rpx;
height: 120rpx;
margin-right: 20rpx;
object-fit: cover;
border-radius: 8rpx;
}
.content-info {
flex: 1;
.content-title {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
line-height: 1.3;
color: #333;
}
.content-desc {
display: block;
display: -webkit-box;
margin-bottom: 12rpx;
overflow: hidden;
font-size: 22rpx;
line-height: 1.4;
color: #666;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.content-meta {
display: flex;
align-items: center;
justify-content: space-between;
.content-category,
.content-date {
padding: 4rpx 12rpx;
font-size: 20rpx;
color: #2196f3;
background: #e3f2fd;
border-radius: 12rpx;
}
.content-views {
font-size: 20rpx;
color: #999;
}
}
}
.content-action {
display: flex;
align-items: center;
margin-left: 16rpx;
}
}
</style>

View File

@ -0,0 +1,571 @@
<template>
<view class="exercise-management">
<!-- 今日运动概览 -->
<view class="exercise-summary">
<view class="summary-header">
<text class="date">{{ formatDate(currentDate) }}</text>
<text class="weather"> 22°C 适宜运动</text>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value">{{ todayStats.steps }}</text>
<text class="stat-label">步数</text>
<text class="stat-unit"></text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.calories }}</text>
<text class="stat-label">消耗</text>
<text class="stat-unit">kcal</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.duration }}</text>
<text class="stat-label">时长</text>
<text class="stat-unit">分钟</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.distance }}</text>
<text class="stat-label">距离</text>
<text class="stat-unit">公里</text>
</view>
</view>
</view>
<!-- 推荐运动 -->
<view class="recommended-exercises">
<view class="section-title">
<text>推荐运动</text>
<text class="constitution-tag">{{ userConstitution }}</text>
</view>
<view class="exercise-cards">
<view
v-for="exercise in recommendedExercises"
:key="exercise.id"
class="exercise-card"
@click="startExercise(exercise)"
>
<image :src="exercise.image" class="exercise-image" />
<view class="exercise-info">
<text class="exercise-name">{{ exercise.name }}</text>
<text class="exercise-desc">{{ exercise.description }}</text>
<view class="exercise-meta">
<text class="exercise-duration">{{ exercise.duration }}分钟</text>
<text class="exercise-calories">{{ exercise.calories }}kcal</text>
</view>
</view>
<view class="exercise-action">
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
<!-- 运动计划 -->
<view class="exercise-plan">
<view class="section-title">本周计划</view>
<view class="plan-calendar">
<view
v-for="day in weekPlan"
:key="day.date"
class="calendar-day"
:class="{ today: day.isToday, completed: day.completed }"
>
<text class="day-name">{{ day.dayName }}</text>
<text class="day-date">{{ day.date }}</text>
<view v-if="day.exercise" class="day-exercise">
<text>{{ day.exercise }}</text>
</view>
<view class="day-status">
<uni-icons
v-if="day.completed"
type="checkmarkempty"
size="16"
color="#4CAF50"
></uni-icons>
</view>
</view>
</view>
</view>
<!-- 最近运动记录 -->
<view class="recent-records">
<view class="section-title">最近记录</view>
<view class="record-list">
<view v-for="record in recentRecords" :key="record.id" class="record-item">
<view class="record-icon" :style="{ backgroundColor: record.color }">
<text>{{ record.type.charAt(0) }}</text>
</view>
<view class="record-info">
<text class="record-name">{{ record.name }}</text>
<text class="record-time">{{ record.time }}</text>
</view>
<view class="record-stats">
<text class="record-duration">{{ record.duration }}分钟</text>
<text class="record-calories">{{ record.calories }}kcal</text>
</view>
</view>
</view>
</view>
<!-- 底部操作 -->
<view class="bottom-actions">
<button class="action-btn primary" @click="quickRecord">快速记录</button>
<button class="action-btn secondary" @click="viewHistory">查看历史</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
interface ExerciseStats {
steps: number;
calories: number;
duration: number;
distance: number;
}
interface RecommendedExercise {
id: string;
name: string;
description: string;
duration: number;
calories: number;
image: string;
type: string;
}
interface ExerciseRecord {
id: string;
name: string;
type: string;
duration: number;
calories: number;
time: string;
color: string;
}
interface WeekDay {
date: number;
dayName: string;
isToday: boolean;
completed: boolean;
exercise?: string;
}
const currentDate = ref(new Date());
const userConstitution = ref("气虚体质");
const todayStats = ref<ExerciseStats>({
steps: 6542,
calories: 285,
duration: 45,
distance: 4.2,
});
const recommendedExercises = ref<RecommendedExercise[]>([
{
id: "1",
name: "太极拳",
description: "柔和缓慢,适合气虚体质调理",
duration: 30,
calories: 120,
image: "/static/images/exercise/taichi.jpg",
type: "traditional",
},
{
id: "2",
name: "八段锦",
description: "传统养生功法,强身健体",
duration: 20,
calories: 80,
image: "/static/images/exercise/baduanjin.jpg",
type: "traditional",
},
{
id: "3",
name: "散步",
description: "温和有氧运动,促进气血循环",
duration: 40,
calories: 150,
image: "/static/images/exercise/walking.jpg",
type: "cardio",
},
{
id: "4",
name: "瑜伽",
description: "伸展身体,平静心神",
duration: 45,
calories: 180,
image: "/static/images/exercise/yoga.jpg",
type: "flexibility",
},
]);
const weekPlan = ref<WeekDay[]>([
{ date: 18, dayName: "周一", isToday: false, completed: true, exercise: "太极拳" },
{ date: 19, dayName: "周二", isToday: false, completed: true, exercise: "散步" },
{ date: 20, dayName: "周三", isToday: false, completed: false, exercise: "八段锦" },
{ date: 21, dayName: "周四", isToday: true, completed: false, exercise: "瑜伽" },
{ date: 22, dayName: "周五", isToday: false, completed: false, exercise: "散步" },
{ date: 23, dayName: "周六", isToday: false, completed: false, exercise: "太极拳" },
{ date: 24, dayName: "周日", isToday: false, completed: false, exercise: "休息" },
]);
const recentRecords = ref<ExerciseRecord[]>([
{
id: "1",
name: "太极拳练习",
type: "传统运动",
duration: 30,
calories: 120,
time: "今天 08:00",
color: "#4CAF50",
},
{
id: "2",
name: "晨间散步",
type: "有氧运动",
duration: 25,
calories: 95,
time: "昨天 07:30",
color: "#2196F3",
},
{
id: "3",
name: "八段锦",
type: "传统运动",
duration: 20,
calories: 80,
time: "前天 19:00",
color: "#FF9800",
},
]);
const formatDate = (date: Date) => {
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = ["日", "一", "二", "三", "四", "五", "六"][date.getDay()];
return `${month}${day}日 周${weekDay}`;
};
const startExercise = (exercise: RecommendedExercise) => {
uni.navigateTo({
url: `/pages/health/exercise/detail?id=${exercise.id}`,
});
};
const quickRecord = () => {
uni.navigateTo({
url: "/pages/health/exercise/record",
});
};
const viewHistory = () => {
uni.navigateTo({
url: "/pages/health/exercise/history",
});
};
</script>
<style lang="scss" scoped>
.exercise-management {
min-height: 100vh;
padding: 20rpx;
background-color: #f8f9fa;
}
.exercise-summary {
padding: 30rpx;
margin-bottom: 20rpx;
color: white;
background: linear-gradient(135deg, #2196f3, #1976d2);
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(33, 150, 243, 0.3);
}
.summary-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.date {
font-size: 28rpx;
font-weight: 600;
}
.weather {
font-size: 22rpx;
opacity: 0.9;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
}
.stat-item {
text-align: center;
.stat-value {
display: block;
margin-bottom: 8rpx;
font-size: 32rpx;
font-weight: 600;
}
.stat-label {
display: block;
margin-bottom: 4rpx;
font-size: 22rpx;
opacity: 0.8;
}
.stat-unit {
font-size: 18rpx;
opacity: 0.7;
}
}
.recommended-exercises {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
.constitution-tag {
padding: 8rpx 16rpx;
font-size: 22rpx;
font-weight: normal;
color: #4caf50;
background: #e8f5e8;
border-radius: 20rpx;
}
}
.exercise-cards {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.exercise-card {
display: flex;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
transition: all 0.3s;
&:active {
transform: scale(0.98);
}
.exercise-image {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.exercise-info {
flex: 1;
.exercise-name {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.exercise-desc {
display: block;
margin-bottom: 12rpx;
font-size: 22rpx;
color: #666;
}
.exercise-meta {
display: flex;
gap: 20rpx;
.exercise-duration,
.exercise-calories {
font-size: 20rpx;
color: #4caf50;
}
}
}
.exercise-action {
display: flex;
align-items: center;
}
}
.exercise-plan {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.plan-calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12rpx;
}
.calendar-day {
position: relative;
padding: 16rpx 8rpx;
text-align: center;
background: #f8f9fa;
border-radius: 12rpx;
&.today {
background: #e3f2fd;
border: 2rpx solid #2196f3;
}
&.completed {
background: #e8f5e8;
}
.day-name {
display: block;
margin-bottom: 8rpx;
font-size: 20rpx;
color: #666;
}
.day-date {
display: block;
margin-bottom: 8rpx;
font-size: 24rpx;
font-weight: 600;
color: #333;
}
.day-exercise {
margin-bottom: 8rpx;
font-size: 18rpx;
color: #4caf50;
}
.day-status {
position: absolute;
top: 8rpx;
right: 8rpx;
}
}
.recent-records {
padding: 30rpx;
margin-bottom: 20rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.record-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.record-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
.record-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
font-size: 24rpx;
font-weight: 600;
color: white;
border-radius: 50%;
}
.record-info {
flex: 1;
.record-name {
display: block;
margin-bottom: 8rpx;
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.record-time {
font-size: 22rpx;
color: #666;
}
}
.record-stats {
text-align: right;
.record-duration {
display: block;
margin-bottom: 4rpx;
font-size: 24rpx;
color: #333;
}
.record-calories {
font-size: 20rpx;
color: #4caf50;
}
}
}
.bottom-actions {
display: flex;
gap: 20rpx;
padding: 30rpx 0;
.action-btn {
flex: 1;
height: 88rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
border-radius: 44rpx;
&.primary {
color: white;
background: #2196f3;
}
&.secondary {
color: #666;
background: #f0f0f0;
}
}
}
</style>

View File

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

View File

@ -0,0 +1,589 @@
<template>
<view class="health-dashboard">
<!-- 顶部状态栏 -->
<view class="health-header">
<view class="user-greeting">
<text class="greeting-text">您好{{ userInfo.name || "用户" }}</text>
<text class="health-score">健康评分{{ healthScore }}/100</text>
</view>
<view class="health-ring">
<view class="ring-progress" :style="{ '--progress': healthScore }">
<text class="ring-text">{{ healthScore }}</text>
</view>
</view>
</view>
<!-- 快捷功能区 -->
<view class="quick-actions">
<scroll-view scroll-x class="action-scroll">
<view class="action-list">
<view
v-for="action in quickActions"
:key="action.id"
class="action-item"
@click="navigateToModule(action.route)"
>
<view class="action-icon">
<text class="icon">{{ action.icon }}</text>
</view>
<text class="action-name">{{ action.name }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 健康数据概览 -->
<view class="health-overview">
<view class="overview-title">今日健康数据</view>
<view class="overview-grid">
<view
v-for="item in healthData"
:key="item.key"
class="data-item"
@click="navigateToDetail(item.route)"
>
<view class="data-icon">
<text class="icon">{{ item.icon }}</text>
</view>
<view class="data-content">
<view class="data-value-wrapper">
<text class="data-value">{{ item.value }}</text>
<text class="data-unit">{{ item.unit }}</text>
</view>
<text class="data-label">{{ item.label }}</text>
</view>
<view class="data-trend" :class="item.trend">
<text>{{ getTrendText(item.trend) }}</text>
</view>
</view>
</view>
</view>
<!-- 健康提醒 -->
<view v-if="reminders.length > 0" class="health-reminders">
<view class="reminder-title">健康提醒</view>
<view class="reminder-list">
<view
v-for="reminder in reminders"
:key="reminder.id"
class="reminder-item"
@click="handleReminder(reminder)"
>
<view class="reminder-icon" :class="reminder.type">
<text class="icon">{{ getReminderIcon(reminder.type) }}</text>
</view>
<view class="reminder-content">
<text class="reminder-text">{{ reminder.message }}</text>
<text class="reminder-time">{{ formatTime(reminder.time) }}</text>
</view>
<view class="reminder-action">
<button size="mini" type="primary">处理</button>
</view>
</view>
</view>
</view>
<!-- 健康资讯 -->
<view class="health-news">
<view class="news-header">
<text class="news-title">健康资讯</text>
<text class="news-more" @click="navigateToEducation">查看更多</text>
</view>
<view class="news-list">
<view
v-for="article in healthArticles"
:key="article.id"
class="news-item"
@click="navigateToArticle(article.id)"
>
<image
class="news-image"
:src="article.thumbnail || '/static/images/health-default.png'"
mode="aspectFill"
/>
<view class="news-content">
<text class="news-title">{{ article.title }}</text>
<view class="news-meta">
<text class="news-category">{{ article.category }}</text>
<text class="news-time">{{ formatDate(article.publishDate) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
const healthScore = ref(85);
const quickActions = ref([
{ id: 1, name: "在线问诊", icon: "👩‍⚕️", route: "/pages/health/consultation/index" },
{ id: 2, name: "健康检测", icon: "🩺", route: "/pages/health/detection/index" },
{ id: 3, name: "慢病管理", icon: "💊", route: "/pages/health/chronic/index" },
{ id: 4, name: "膳食管理", icon: "🍎", route: "/pages/health/diet/index" },
{ id: 5, name: "运动管理", icon: "🏃‍♂️", route: "/pages/health/exercise/index" },
{ id: 6, name: "体质辩识", icon: "☯️", route: "/pages/health/constitution/index" },
{ id: 7, name: "中医百科", icon: "📚", route: "/pages/health/encyclopedia/index" },
{ id: 8, name: "健康讲堂", icon: "🎓", route: "/pages/health/education/index" },
]);
const healthData = ref([
{
key: "heart_rate",
label: "心率",
value: "72",
unit: "bpm",
icon: "❤️",
trend: "stable",
route: "/pages/health/detection/vitals",
},
{
key: "blood_pressure",
label: "血压",
value: "120/80",
unit: "mmHg",
icon: "🩸",
trend: "down",
route: "/pages/health/detection/vitals",
},
{
key: "weight",
label: "体重",
value: "65.5",
unit: "kg",
icon: "⚖️",
trend: "up",
route: "/pages/health/detection/vitals",
},
{
key: "steps",
label: "步数",
value: "8,234",
unit: "步",
icon: "👣",
trend: "up",
route: "/pages/health/exercise/record",
},
]);
const reminders = ref([
{
id: 1,
type: "medication",
message: "该服用降压药了",
time: new Date(),
},
{
id: 2,
type: "exercise",
message: "今日运动目标未完成",
time: new Date(),
},
]);
const healthArticles = ref([
{
id: "1",
title: "春季养生小贴士",
category: "养生保健",
publishDate: new Date().toISOString(),
thumbnail: "/static/images/article1.jpg",
},
{
id: "2",
title: "如何科学减肥",
category: "健康生活",
publishDate: new Date().toISOString(),
thumbnail: "/static/images/article2.jpg",
},
]);
const userInfo = ref({ name: "张三" });
const navigateToModule = (route: string) => {
uni.navigateTo({ url: route });
};
const navigateToDetail = (route: string) => {
uni.navigateTo({ url: route });
};
const navigateToEducation = () => {
uni.navigateTo({ url: "/pages/health/education/index" });
};
const navigateToArticle = (articleId: string) => {
uni.navigateTo({ url: `/pages/health/education/articles/detail?id=${articleId}` });
};
const handleReminder = (reminder: any) => {
console.log("处理提醒:", reminder);
};
const getTrendText = (trend: string) => {
switch (trend) {
case "up":
return "↗";
case "down":
return "↘";
default:
return "→";
}
};
const getReminderIcon = (type: string) => {
switch (type) {
case "medication":
return "💊";
case "exercise":
return "🏃";
case "diet":
return "🍎";
default:
return "⏰";
}
};
const formatTime = (time: Date) => {
return time.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
};
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("zh-CN");
};
</script>
<style lang="scss" scoped>
.health-dashboard {
min-height: 100vh;
padding: 20rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.health-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
margin-bottom: 30rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20rpx;
.user-greeting {
.greeting-text {
display: block;
margin-bottom: 10rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.health-score {
font-size: 28rpx;
color: #1aad19;
}
}
.health-ring {
.ring-progress {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
background: conic-gradient(
#1aad19 0deg,
#1aad19 calc(var(--progress) * 3.6deg),
#e0e0e0 calc(var(--progress) * 3.6deg)
);
border-radius: 50%;
&::before {
position: absolute;
width: 80rpx;
height: 80rpx;
content: "";
background: white;
border-radius: 50%;
}
.ring-text {
z-index: 1;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
}
}
.quick-actions {
margin-bottom: 30rpx;
.action-scroll {
white-space: nowrap;
}
.action-list {
display: flex;
padding: 0 20rpx;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 140rpx;
padding: 30rpx 20rpx;
margin-right: 20rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20rpx;
transition: transform 0.3s ease;
&:active {
transform: scale(0.95);
}
.action-icon {
margin-bottom: 15rpx;
.icon {
font-size: 40rpx;
}
}
.action-name {
font-size: 24rpx;
color: #333;
text-align: center;
}
}
}
.health-overview {
padding: 30rpx;
margin-bottom: 30rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20rpx;
.overview-title {
margin-bottom: 30rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.overview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.data-item {
position: relative;
display: flex;
align-items: center;
padding: 25rpx;
background: #f8f9fa;
border-radius: 15rpx;
.data-icon {
margin-right: 20rpx;
.icon {
font-size: 32rpx;
}
}
.data-content {
flex: 1;
.data-value-wrapper {
display: flex;
align-items: baseline;
.data-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.data-unit {
margin-left: 5rpx;
font-size: 20rpx;
color: #999;
}
}
.data-label {
display: block;
margin-top: 5rpx;
font-size: 24rpx;
color: #666;
}
}
.data-trend {
position: absolute;
top: 15rpx;
right: 15rpx;
font-size: 24rpx;
&.up {
color: #ff4757;
}
&.down {
color: #1dd1a1;
}
&.stable {
color: #ffa502;
}
}
}
}
.health-reminders {
padding: 30rpx;
margin-bottom: 30rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20rpx;
.reminder-title {
margin-bottom: 30rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.reminder-item {
display: flex;
align-items: center;
padding: 25rpx;
margin-bottom: 20rpx;
background: #fff;
border-radius: 15rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.reminder-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
border-radius: 50%;
&.medication {
background: #e8f5e8;
}
&.exercise {
background: #e8f4fd;
}
&.diet {
background: #fff2e8;
}
.icon {
font-size: 30rpx;
}
}
.reminder-content {
flex: 1;
.reminder-text {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
color: #333;
}
.reminder-time {
font-size: 24rpx;
color: #999;
}
}
}
}
.health-news {
padding: 30rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 20rpx;
.news-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
.news-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.news-more {
font-size: 26rpx;
color: #007aff;
}
}
.news-item {
display: flex;
padding: 20rpx;
margin-bottom: 20rpx;
background: #fff;
border-radius: 15rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.news-image {
width: 120rpx;
height: 90rpx;
margin-right: 20rpx;
border-radius: 10rpx;
}
.news-content {
flex: 1;
.news-title {
display: -webkit-box;
margin-bottom: 15rpx;
overflow: hidden;
font-size: 28rpx;
line-height: 1.4;
color: #333;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.news-meta {
display: flex;
justify-content: space-between;
.news-category,
.news-time {
font-size: 22rpx;
color: #999;
}
.news-category {
padding: 5rpx 15rpx;
background: #f0f0f0;
border-radius: 10rpx;
}
}
}
}
}
</style>

View File

@ -0,0 +1,10 @@
<template>
<div class="health-index">
<h1>Health Index Page</h1>
<p>Welcome to the health index page!</p>
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>

309
src/pages/index/index.vue Normal file
View File

@ -0,0 +1,309 @@
<template>
<view style="width: 100%; height: var(--status-bar-height)" />
<view class="home">
<wd-swiper
v-model:current="current"
:list="swiperList"
autoplay
@click="handleClick"
@change="onChange"
/>
<!-- 快捷导航 -->
<wd-grid clickable :column="4" class="mt-2">
<wd-grid-item
v-for="(item, index) in navList"
:key="index"
use-slot
link-type="navigateTo"
:url="item.url"
>
<view class="p-2">
<image class="w-72rpx h-72rpx rounded-8rpx" :src="item.icon" />
</view>
<view class="text">{{ item.title }}</view>
</wd-grid-item>
</wd-grid>
<!-- 通知公告 -->
<wd-notice-bar
text="中医的慢病管理系统 是一个基于 Vue3 + UniApp + TypeScript 的多端慢病管理系统支持Android、IOS、鸿蒙、微信小程序等平台旨在帮助患者更好地管理慢性疾病。 服务器当前维护中,具体上线时间请关注官方通知。"
color="#34D19D"
type="info"
>
<template #prefix>
<wd-tag color="#FAA21E" bg-color="#FAA21E" plain custom-style="margin-right:10rpx">
通知公告
</wd-tag>
</template>
</wd-notice-bar>
<!-- 数据统计 -->
<wd-grid :column="4" :gutter="4">
<wd-grid-item use-slot custom-class="custom-item">
<view class="flex justify-start pl-5">
<view class="flex-center">
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/blood_pressure.png" />
<view class="ml-5 text-left">
<view class="font-bold">血压</view>
<view class="mt-2">
{{ VitalSigns.bloodPressure.systolic }}/{{ VitalSigns.bloodPressure.diastolic }}
</view>
</view>
</view>
</view>
</wd-grid-item>
<wd-grid-item use-slot custom-class="custom-item">
<view class="flex justify-start pl-5">
<view class="flex-center">
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/blood_glucose.png" />
<view class="ml-5 text-left">
<view class="font-bold">血糖</view>
<view class="mt-2">{{ VitalSigns.bloodGlucose }}</view>
</view>
</view>
</view>
</wd-grid-item>
<wd-grid-item use-slot custom-class="custom-item">
<view class="flex justify-start pl-5">
<view class="flex-center">
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/spo2.png" />
<view class="ml-5 text-left">
<view class="font-bold">血氧饱和度</view>
<view class="mt-2">{{ VitalSigns.bloodOxygen }}</view>
</view>
</view>
</view>
</wd-grid-item>
<wd-grid-item use-slot custom-class="custom-item">
<view class="flex justify-start pl-5">
<view class="flex-center">
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/steps.png" />
<view class="ml-5 text-left">
<view class="font-bold">今日步数</view>
<view class="mt-2">{{ VitalSigns.steps }}</view>
</view>
</view>
</view>
</wd-grid-item>
</wd-grid>
<wd-card>
<template #title>
<view class="flex-between">
<view>健康趋势</view>
<view>
<!-- <wd-radio-group-->
<!-- v-model="recentDaysRange"-->
<!-- shape="button"-->
<!-- inline-->
<!-- @change="handleDataRangeChange"-->
<!-- >-->
<!-- <wd-radio :value="7">近7天</wd-radio>-->
<!-- <wd-radio :value="15">近15天</wd-radio>-->
<!-- </wd-radio-group>-->
</view>
</view>
</template>
<view class="charts-box">
<qiun-data-charts type="area" :chartData="chartData" :opts="chartOpts" />
</view>
</wd-card>
</view>
</template>
<script setup lang="ts">
// import { dayjs } from "wot-design-uni";
import { VitalSignsData } from "@/api/health/detection";
const current = ref<number>(0);
const VitalSigns = ref<VitalSignsData>({
heartRate: 75,
bodyTemperature: 36.5,
weight: 70,
height: 175,
bloodPressure: {
systolic: 120,
diastolic: 80,
},
bloodGlucose: 20,
bloodOxygen: 98,
steps: 9004,
});
const chartData = ref({
categories: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"], // 时间点
series: [
{
name: "血压(高压)",
color: "#FF6B6B",
data: [120, 118, 125, 130, 128, 122],
},
{
name: "血压(低压)",
color: "#FFA3A3",
data: [80, 78, 82, 85, 83, 79],
},
{
name: "血氧(%)",
color: "#4ECDC4",
data: [98, 97, 96, 99, 98, 97],
},
{
name: "血糖(mmol/L)",
color: "#45B7D1",
data: [5.2, 5.0, 6.1, 5.8, 5.5, 5.3],
},
{
name: "步数(千步)",
color: "#6A6BFF",
data: [0, 0.5, 4.2, 6.7, 8.9, 10.2],
},
],
});
const chartOpts = ref({
padding: [20, 20, 30, 40], // 调整padding确保标签可见
xAxis: {
fontSize: 10,
rotateLabel: true,
rotateAngle: 45, // 增加角度防止重叠
boundaryGap: true, // 让图表从坐标轴开始
},
yAxis: {
disabled: false, // 启用y轴
splitNumber: 5,
data: [
{ min: 0 }, // 步数和血氧从0开始
{ min: 70, max: 100 }, // 血氧范围
{ min: 4, max: 10 }, // 血糖范围
{ min: 70, max: 140 }, // 血压范围
],
gridType: "dash", // 虚线网格
dashLength: 4,
},
dataLabel: true, // 显示数据标签
legend: {
position: "bottom", // 图例放在底部
fontSize: 10,
},
extra: {
area: {
type: "curve",
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: "solid", // 高亮时显示实线
animation: true, // 添加动画效果
},
},
});
// 日期范围
const recentDaysRange = ref(7);
const swiperList = ref(["http://115.190.102.167:5324/banner.png"]);
// 快捷导航列表
const navList = reactive([
{
icon: "/static/icons/user.png",
title: "在线问诊",
url: "/pages/health/consultation/index",
prem: "sys:user:query",
},
{
icon: "/static/icons/role.png",
title: "慢病管理",
url: "/pages/health/chronic/index",
prem: "sys:role:query",
},
{
icon: "/static/icons/notice.png",
title: "中医百科",
url: "/pages/health/encyclopedia/index",
prem: "sys:notice:query",
},
{
icon: "/static/icons/setting.png",
title: "健康讲堂",
url: "/pages/health/education/index",
prem: "sys:config:query",
},
]);
function handleClick(e: any) {
console.log(e);
}
function onChange(e: any) {
console.log(e);
}
// 加载访问统计数据
// const loadVisitStatsData = async () => {
// LogAPI.getVisitStats().then((data) => {
// visitStatsData.value = data;
// });
// };
// 加载访问趋势数据
const loadVisitTrendData = () => {
const endDate = new Date();
const startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - recentDaysRange.value + 1);
// const visitTrendQuery = {
// startDate: dayjs(startDate).format("YYYY-MM-DD"),
// endDate: dayjs(endDate).format("YYYY-MM-DD"),
// };
// LogAPI.getVisitTrend(visitTrendQuery).then((data) => {
// const res = {
// categories: data.dates,
// series: [
// {
// name: "访客数(UV)",
// data: data.ipList,
// },
// {
// name: "浏览量(PV)",
// data: data.pvList,
// },
// ],
// };
// chartData.value = JSON.parse(JSON.stringify(res));
// });
};
// // 数据范围变化
// const handleDataRangeChange = ({ value }: { value: number }) => {
// console.log("handleDataRangeChange", value);
// recentDaysRange.value = value;
// loadVisitTrendData();
// };
onReady(() => {
// loadVisitStatsData();
loadVisitTrendData();
});
</script>
<style setup lang="scss">
.home {
padding: 10rpx 10rpx;
:deep(.custom-item) {
height: 80px !important;
}
:deep(.wd-card) {
margin: 10rpx 0 !important;
}
}
.charts-box {
width: 100%;
height: 300px;
}
</style>

393
src/pages/login/index.vue Normal file
View File

@ -0,0 +1,393 @@
<template>
<view class="login-container">
<!-- 背景图 -->
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-bg" />
<!-- Logo和标题区域 -->
<view class="header">
<image src="/static/logo.png" class="logo" />
<text class="title">中医慢病管理系统</text>
<text class="subtitle">一种中医慢病管理系统实现方案</text>
</view>
<!-- 登录表单区域 -->
<view class="login-card">
<view class="form-wrap">
<wd-form ref="loginFormRef" :model="loginFormData">
<!-- 用户名输入框 -->
<view class="form-item">
<wd-icon name="user" size="22" color="#165DFF" class="input-icon" />
<input v-model="loginFormData.username" class="form-input" placeholder="请输入用户名" />
<wd-icon
v-if="loginFormData.username"
name="close-fill"
size="18"
color="#9ca3af"
class="clear-icon"
@click="loginFormData.username = ''"
/>
</view>
<view class="divider"></view>
<!-- 密码输入框 -->
<view class="form-item">
<wd-icon name="lock-on" size="22" color="#165DFF" class="input-icon" />
<input
v-model="loginFormData.password"
class="form-input"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
placeholder-style="color: #9ca3af; font-weight: normal;"
/>
<wd-icon
:name="showPassword ? 'eye-open' : 'eye-close'"
size="18"
color="#9ca3af"
class="eye-icon"
@click="showPassword = !showPassword"
/>
</view>
<view class="divider"></view>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="loading"
:style="loading ? 'opacity: 0.7;' : ''"
@click="handleLogin"
>
登录
</button>
</wd-form>
<!-- 微信登录 -->
<view class="other-login">
<view class="other-login-title">
<view class="line"></view>
<text class="text">其他登录方式</text>
<view class="line"></view>
</view>
<view class="wechat-login" @click="handleWechatLogin">
<view class="wechat-icon-wrapper">
<image src="/static/icons/weixin.png" class="wechat-icon" />
</view>
</view>
</view>
<!-- 底部协议 -->
<view class="agreement">
<text class="text">登录即同意</text>
<text class="link" @click="navigateToUserAgreement">用户协议</text>
<text class="text"></text>
<text class="link" @click="navigateToPrivacy">隐私政策</text>
</view>
</view>
</view>
<wd-toast />
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
import { type LoginFormData } from "@/api/auth";
import { useUserStore } from "@/store/modules/user";
import { useToast } from "wot-design-uni";
import { ref } from "vue";
const loginFormRef = ref();
const toast = useToast();
const loading = ref(false);
const userStore = useUserStore();
const showPassword = ref(false);
// 登录表单数据
const loginFormData = ref<LoginFormData>({
username: "admin",
password: "123456",
});
// 获取重定向参数
const redirect = ref("");
onLoad((options) => {
if (options) {
redirect.value = options.redirect ? decodeURIComponent(options.redirect) : "/pages/index/index";
} else {
redirect.value = "/pages/index/index";
}
});
// 登录处理
const handleLogin = () => {
if (loading.value) return;
loading.value = true;
userStore
.login(loginFormData.value)
.then(() => userStore.getInfo())
.then(() => {
toast.success("登录成功");
// 检查用户信息是否完整
if (!userStore.isUserInfoComplete()) {
// 信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
}
})
.catch((error) => {
toast.error(error?.message || "登录失败");
})
.finally(() => {
loading.value = false;
});
};
// 微信登录处理
const handleWechatLogin = async () => {
if (loading.value) return;
loading.value = true;
try {
// #ifdef MP-WEIXIN
// 获取微信登录的临时 code
const { code } = await uni.login({
provider: "weixin",
});
// 调用后端接口进行登录认证
const result = await userStore.loginByWechat(code);
if (result) {
// 获取用户信息
await userStore.getInfo();
toast.success("登录成功");
// 检查用户信息是否完整
if (!userStore.isUserInfoComplete()) {
// 如果信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
}
}
// #endif
// #ifndef MP-WEIXIN
toast.error("当前环境不支持微信登录");
// #endif
} catch (error: any) {
toast.error(error?.message || "微信登录失败");
} finally {
loading.value = false;
}
};
// 跳转到用户协议页面
const navigateToUserAgreement = () => {
uni.navigateTo({
url: "/pages/mine/user-agreement/index",
});
};
// 跳转到隐私政策页面
const navigateToPrivacy = () => {
uni.navigateTo({
url: "/pages/mine/privacy/index",
});
};
</script>
<style lang="scss" scoped>
.login-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
overflow: hidden;
}
.login-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.header {
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 120rpx;
}
.logo {
width: 140rpx;
height: 140rpx;
margin-bottom: 20rpx;
}
.title {
margin-bottom: 10rpx;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 28rpx;
color: #ffffff;
text-align: center;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.login-card {
z-index: 2;
display: flex;
flex-direction: column;
width: 90%;
margin-top: 80rpx;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 24rpx;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
}
.form-wrap {
padding: 40rpx;
}
.form-item {
position: relative;
display: flex;
align-items: center;
padding: 24rpx 0;
}
.input-icon {
margin-right: 20rpx;
}
.form-input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
line-height: 60rpx;
color: #333;
}
.clear-icon,
.eye-icon {
padding: 10rpx;
}
.divider {
height: 1px;
margin: 0;
background-color: rgba(0, 0, 0, 0.06);
}
.login-btn {
width: 100%;
height: 90rpx;
margin-top: 60rpx;
font-size: 32rpx;
line-height: 90rpx;
color: #fff;
background: linear-gradient(90deg, #165dff, #4080ff);
border: none;
border-radius: 45rpx;
box-shadow: 0 8rpx 20rpx rgba(22, 93, 255, 0.3);
transition: all 0.3s;
}
.login-btn:active {
box-shadow: 0 4rpx 10rpx rgba(22, 93, 255, 0.2);
transform: translateY(2rpx);
}
.other-login {
margin-top: 60rpx;
}
.other-login-title {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.line {
flex: 1;
height: 1px;
background-color: rgba(0, 0, 0, 0.08);
}
.text {
padding: 0 30rpx;
font-size: 26rpx;
color: #9ca3af;
}
.wechat-login {
display: flex;
justify-content: center;
margin-bottom: 30rpx;
}
.wechat-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
height: 90rpx;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.wechat-icon {
width: 60rpx;
height: 60rpx;
}
.agreement {
display: flex;
justify-content: center;
margin-top: 30rpx;
font-size: 24rpx;
}
.agreement .text {
padding: 0 4rpx;
color: #9ca3af;
}
.agreement .link {
color: #165dff;
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<view class="about-container">
<!-- 顶部 Logo 区域 -->
<view class="logo-section">
<image class="logo" src="/static/logo.png" mode="aspectFit" />
<text class="app-name">vue-uniapp-template</text>
<text class="version">版本 {{ version }}</text>
</view>
<!-- 公司信息区域 -->
<view class="company-info">
<text class="company-name">有来开源组织</text>
<view class="divider" />
<text class="company-desc">专注于快速构建和高效开发的应用解决方案</text>
</view>
<!-- 信息列表 -->
<view class="info-list">
<view class="list-header">
<text class="header-title">优质项目</text>
</view>
<view class="info-item">
<view class="item-content">
<text class="item-label">vue3-element-admin</text>
<text class="item-desc">
基于 Vue3 + Vite5+ TypeScript5 + Element-Plus + Pinia
等主流技术栈构建的免费开源的中后台管理的前端模板
</text>
</view>
</view>
<view class="info-item">
<view class="item-content">
<text class="item-label">vue-uniapp-template</text>
<text class="item-desc">
基于 uni-app + Vue 3 + TypeScript 的项目集成了 ESLintPrettierStylelintHusky
Commitlint 等工具确保代码规范与质量
</text>
</view>
</view>
<view class="info-item">
<view class="item-content">
<text class="item-label">youlai-boot</text>
<text class="item-desc">
基于 JDK 17Spring Boot 3Spring Security 6JWTRedisMybatis-PlusKnife4jVue
3Element-Plus 构建的前后端分离单体权限管理系统
</text>
</view>
</view>
</view>
<!-- 底部版权信息 -->
<view class="copyright">
<text>Copyright © {{ getYear() }} 有来开源组织</text>
<text>All Rights Reserved</text>
</view>
</view>
</template>
<script lang="ts" setup>
const version = ref("1.0.0");
const getYear = () => {
return new Date().getFullYear();
};
onMounted(() => {
// #ifdef MP-WEIXIN
version.value = uni.getSystemInfoSync().appVersion;
// #endif
});
</script>
<style lang="scss" scoped>
.about-container {
min-height: 100vh;
padding: 20px;
background-color: #f5f5f5;
}
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 0;
.logo {
width: 80px;
height: 80px;
margin-bottom: 15px;
}
.app-name {
margin-bottom: 8px;
font-size: 20px;
font-weight: bold;
}
.version {
font-size: 14px;
color: #666;
}
}
.company-info {
padding: 30px;
margin: 20px 0;
text-align: center;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.company-name {
margin-bottom: 15px;
font-size: 20px;
font-weight: 600;
color: #333;
}
.divider {
width: 40px;
height: 2px;
margin: 15px auto;
background-color: #409eff;
}
.company-desc {
padding: 0 10px;
margin-top: 15px;
overflow: hidden;
font-size: 15px;
line-height: 1.6;
color: #666;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.info-list {
margin: 20px 0;
background-color: #fff;
border-radius: 8px;
.list-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
.header-title {
font-size: 17px;
font-weight: 600;
color: #333;
}
}
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
.item-content {
flex: 1;
padding-right: 15px;
}
.item-label {
display: block;
margin-bottom: 4px;
font-size: 16px;
color: #333;
}
.item-desc {
display: block;
font-size: 13px;
line-height: 1.4;
color: #999;
}
.item-value {
font-size: 16px;
color: #999;
}
}
}
.copyright {
padding: 20px 0;
text-align: center;
text {
display: block;
font-size: 12px;
line-height: 1.5;
color: #999;
}
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<view class="faq-container">
<view class="wechat">
<view class="tips">
<text>长按关注有来技术公众号获取交流群二维码</text>
</view>
<view class="flex-center">
<image
class="w-158px h-158px"
:show-menu-by-longpress="true"
src="/static/images/qrcode-official.png"
mode="aspectFit"
/>
</view>
<view>
<text>如果交流群的二维码过期请加微信(</text>
<text :user-select="true" :selectable="true">haoxianrui</text>
<text>)并备注前端后端全栈以获取最新二维码</text>
</view>
<view>
<text>为确保交流群质量防止营销广告人群混入我们采取了此措施望各位理解</text>
</view>
</view>
<wd-collapse v-model="value">
<wd-collapse-item title="开源项目issues" name="item1">
<!-- #ifdef H5 -->
<a href="https://gitee.com/youlaiorg/vue-uniapp-template/issues">#issues</a>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<text :user-select="true">https://gitee.com/youlaiorg/vue-uniapp-template/issues</text>
<!-- #endif -->
</wd-collapse-item>
<wd-collapse-item title="小程序分包" name="item2">
<view>
<text>
分包主要是因为小程序平台对主包大小有限制微信小程序的规则是主包不超过2M每个分包不超过2M总体积一共不能超过20M
分包不需要按照业务模块来分可以将多个业务模块放入一个分包中直到这个分包达到小程序的大小限制才考虑下一个分包
uniapp的用法与微信官方文档一样具体参见
</text>
<!-- #ifdef H5 -->
<a
href="https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html"
>
微信官方文档-分包
</a>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<text :user-select="true" :selectable="true">
https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html
</text>
<!-- #endif -->
</view>
<view class="mt-15rpx">
<text>
以下是一个简单示例以下示例中创建了两个分包分包a中包含两个页面分包b中包含一个页面
</text>
<text class="mt-15rpx">
请注意如果想把分包页面中使用的组件打包到分包中则需要将组件放入对应的分包目录下否则组件会被打包到主包中
</text>
</view>
<view class="mt-15rpx">
<text>目录结构</text>
</view>
<rich-text :nodes="subListStr" />
<view class="mt-15rpx">
<text>在pages.json文件中声明分包结构</text>
</view>
<rich-text :nodes="pagesStr" />
</wd-collapse-item>
</wd-collapse>
</view>
</template>
<script lang="ts" setup>
const value = ref<string[]>(["item1"]);
const subListStr = ref<string>(`
<pre style="background-color: #f9f9fa"><code>
|-- components //主包组件目录
|-- pages //主包页面目录
| |-- index
|-- sub-pkg-a
| |-- components //分包组件目录
| |-- pages //分包页面目录
| | |-- cat
| | |-- dog
|-- sub-pkg-b
| |-- components //分包组件目录
| |-- pages //分包页面目录
| | |-- apple</code></pre>
`);
const pagesStr = ref<string>(`<pre style="background-color: #f9f9fa"><code>
{
"pages":[
{
"path": "pages/index",
"style": {
"navigationBarTitleText": "主页"
}
}
],
"subPackages": [
{
"root": "sub-pkg-a",
"pages": [
{
"path": "pages/cat",
"style": {
"navigationBarTitleText": "cat"
}
},
{
"path": "pages/dog",
"style": {
"navigationBarTitleText": "dog"
}
}
]
},
{
"root": "sub-pkg-b",
"pages": [
{
"path": "pages/apple",
"style": {
"navigationBarTitleText": "apple"
}
}
]
}
]
}</code></pre>
`);
onMounted(() => {});
</script>
<style lang="scss" scoped>
.faq-container {
min-height: 100vh;
background-color: #f5f5f5;
.wechat {
padding: 30rpx;
margin: 20px 0;
font-size: 14px;
color: var(--wot-card-content-color, rgba(0, 0, 0, 0.45));
background-color: #fff;
.tips {
font-weight: bold;
text-align: center;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More