created
This commit is contained in:
15
.editorconfig
Normal file
15
.editorconfig
Normal 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
10
.env.development
Normal 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
4
.env.production
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
# API 基础路径,开发环境下的请求前缀
|
||||
VITE_APP_BASE_API = '/prod-api'
|
98
.eslintrc-auto-import.json
Normal file
98
.eslintrc-auto-import.json
Normal 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
26
.gitignore
vendored
Normal 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
1
.husky/commit-msg
Normal file
@ -0,0 +1 @@
|
||||
npx --no-install commitlint --edit $1
|
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@ -0,0 +1,2 @@
|
||||
echo "Running pre-commit hook..."
|
||||
pnpm run lint:lint-staged
|
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
*.min.js
|
||||
dist
|
||||
public
|
||||
node_modules
|
||||
auto-imports.d.ts
|
41
.prettierrc.yaml
Normal file
41
.prettierrc.yaml
Normal 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
5
.stylelintignore
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
*.min.js
|
||||
dist
|
||||
public
|
||||
node_modules
|
55
.stylelintrc.json
Normal file
55
.stylelintrc.json
Normal 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
12
.vscode/settings.json
vendored
Normal 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
21
LICENSE
Normal 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
926
README.md
Normal 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
88
commitlint.config.cjs
Normal 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
129
eslint.config.mjs
Normal 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
24
index.html
Normal 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
142
package.json
Normal 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
8
shims-uni.d.ts
vendored
Normal 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
44
src/App.vue
Normal 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
63
src/api/auth/index.ts
Normal 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
75
src/api/file/index.ts
Normal 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
295
src/api/health/chronic.ts
Normal 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;
|
||||
}
|
163
src/api/health/constitution.ts
Normal file
163
src/api/health/constitution.ts
Normal 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[];
|
||||
}
|
277
src/api/health/consultation.ts
Normal file
277
src/api/health/consultation.ts
Normal 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
212
src/api/health/detection.ts
Normal 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
329
src/api/health/diet.ts
Normal 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
359
src/api/health/education.ts
Normal 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;
|
||||
}
|
253
src/api/health/encyclopedia.ts
Normal file
253
src/api/health/encyclopedia.ts
Normal 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
343
src/api/health/exercise.ts
Normal 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
359
src/api/health/profile.ts
Normal 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
104
src/api/system/config.ts
Normal 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
130
src/api/system/dept.ts
Normal 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
305
src/api/system/dict.ts
Normal 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
123
src/api/system/log.ts
Normal 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
76
src/api/system/menu.ts
Normal 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
201
src/api/system/notice.ts
Normal 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;
|
||||
/** 优先级(L:低,M:中,H:高) */
|
||||
level?: string;
|
||||
/** 目标类型(1-全体 2-指定) */
|
||||
targetType?: number;
|
||||
/** 目标ID合集,以,分割 */
|
||||
targetUserIds?: string;
|
||||
}
|
||||
|
||||
/** 通知公告分页对象 */
|
||||
export interface NoticePageVO {
|
||||
id: string;
|
||||
/** 通知标题 */
|
||||
title?: string;
|
||||
/** 通知内容 */
|
||||
content?: string;
|
||||
/** 通知类型 */
|
||||
type?: number;
|
||||
/** 发布人 */
|
||||
publisherName?: string;
|
||||
/** 优先级(0-低 1-中 2-高) */
|
||||
priority?: number;
|
||||
/** 目标类型(0-全体 1-指定) */
|
||||
targetType?: number;
|
||||
/** 发布状态(0-未发布 1已发布 2已撤回) */
|
||||
publishStatus?: number;
|
||||
/** 发布时间 */
|
||||
publishTime?: Date;
|
||||
/** 撤回时间 */
|
||||
revokeTime?: Date;
|
||||
/** 优先级(L-低 M-中 H-高) */
|
||||
level?: string | number;
|
||||
}
|
||||
|
||||
export interface NoticeDetailVO {
|
||||
/** 通知ID */
|
||||
id?: string;
|
||||
|
||||
/** 通知标题 */
|
||||
title?: string;
|
||||
|
||||
/** 通知内容 */
|
||||
content?: string;
|
||||
|
||||
/** 通知类型 */
|
||||
type?: number;
|
||||
|
||||
/** 发布人 */
|
||||
publisherName?: string;
|
||||
|
||||
/** 优先级(L-低 M-中 H-高) */
|
||||
level?: string;
|
||||
|
||||
/** 发布时间 */
|
||||
publishTime?: Date;
|
||||
|
||||
/** 发布状态 */
|
||||
publishStatus?: number;
|
||||
}
|
138
src/api/system/role.ts
Normal file
138
src/api/system/role.ts
Normal 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
316
src/api/system/user.ts
Normal 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;
|
||||
}
|
82
src/components/cu-date-query/index.vue
Normal file
82
src/components/cu-date-query/index.vue
Normal 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>
|
67
src/components/cu-dict-label/index.vue
Normal file
67
src/components/cu-dict-label/index.vue
Normal 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>
|
141
src/components/cu-dict/index.vue
Normal file
141
src/components/cu-dict/index.vue
Normal 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>
|
174
src/components/cu-picker/index.vue
Normal file
174
src/components/cu-picker/index.vue
Normal 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>
|
196
src/components/da-tree/changelog.md
Normal file
196
src/components/da-tree/changelog.md
Normal 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. 支持多选
|
1209
src/components/da-tree/index.vue
Normal file
1209
src/components/da-tree/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
197
src/components/da-tree/props.ts
Normal file
197
src/components/da-tree/props.ts
Normal 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,
|
||||
},
|
||||
};
|
310
src/components/da-tree/readme.md
Normal file
310
src/components/da-tree/readme.md
Normal 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)
|
153
src/components/da-tree/utils.ts
Normal file
153
src/components/da-tree/utils.ts
Normal 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);
|
||||
}
|
201
src/components/qiun-data-charts/license.md
Normal file
201
src/components/qiun-data-charts/license.md
Normal 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.
|
1691
src/components/qiun-data-charts/qiun-data-charts.vue
Normal file
1691
src/components/qiun-data-charts/qiun-data-charts.vue
Normal file
File diff suppressed because it is too large
Load Diff
44
src/components/qiun-error/qiun-error.vue
Normal file
44
src/components/qiun-error/qiun-error.vue
Normal file
File diff suppressed because one or more lines are too long
158
src/components/qiun-loading/loading1.vue
Normal file
158
src/components/qiun-loading/loading1.vue
Normal 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>
|
164
src/components/qiun-loading/loading2.vue
Normal file
164
src/components/qiun-loading/loading2.vue
Normal 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>
|
171
src/components/qiun-loading/loading3.vue
Normal file
171
src/components/qiun-loading/loading3.vue
Normal 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>
|
219
src/components/qiun-loading/loading4.vue
Normal file
219
src/components/qiun-loading/loading4.vue
Normal 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>
|
226
src/components/qiun-loading/loading5.vue
Normal file
226
src/components/qiun-loading/loading5.vue
Normal 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>
|
32
src/components/qiun-loading/qiun-loading.vue
Normal file
32
src/components/qiun-loading/qiun-loading.vue
Normal 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>
|
122
src/components/todo/TodoItem.vue
Normal file
122
src/components/todo/TodoItem.vue
Normal 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>
|
43
src/components/todo/TodoList.vue
Normal file
43
src/components/todo/TodoList.vue
Normal 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>
|
444
src/components/u-charts/config-echarts.js
Normal file
444
src/components/u-charts/config-echarts.js
Normal 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;
|
676
src/components/u-charts/config-ucharts.js
Normal file
676
src/components/u-charts/config-ucharts.js
Normal 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;
|
201
src/components/u-charts/license.md
Normal file
201
src/components/u-charts/license.md
Normal 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.
|
6
src/components/u-charts/readme.md
Normal file
6
src/components/u-charts/readme.md
Normal 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`文件,以免被强制覆盖。
|
9394
src/components/u-charts/u-charts.js
Normal file
9394
src/components/u-charts/u-charts.js
Normal file
File diff suppressed because it is too large
Load Diff
18
src/components/u-charts/u-charts.min.js
vendored
Normal file
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
10
src/directive/index.ts
Normal 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);
|
||||
}
|
69
src/directive/permission/index.ts
Normal file
69
src/directive/permission/index.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
18
src/enums/ResultCodeEnum.ts
Normal file
18
src/enums/ResultCodeEnum.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 响应码枚举
|
||||
*/
|
||||
export const enum ResultCodeEnum {
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
SUCCESS = "00000",
|
||||
/**
|
||||
* 错误
|
||||
*/
|
||||
ERROR = "B0001",
|
||||
|
||||
/**
|
||||
* 令牌无效或过期
|
||||
*/
|
||||
TOKEN_INVALID = "A0230",
|
||||
}
|
7
src/env.d.ts
vendored
Normal file
7
src/env.d.ts
vendored
Normal 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
7
src/hooks/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 全局Hooks入口文件
|
||||
* 导出所有可用的Hooks
|
||||
*/
|
||||
|
||||
// 导出WebSocket相关Hook
|
||||
export * from "./websocket";
|
285
src/hooks/websocket/core/useStomp.ts
Normal file
285
src/hooks/websocket/core/useStomp.ts
Normal 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,
|
||||
};
|
||||
}
|
10
src/hooks/websocket/index.ts
Normal file
10
src/hooks/websocket/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* WebSocket相关Hook入口文件
|
||||
* 统一导出所有WebSocket相关Hook
|
||||
*/
|
||||
|
||||
// 核心基础Hook
|
||||
export { useStomp } from "./core/useStomp";
|
||||
|
||||
// 业务服务Hook
|
||||
export { useDictSync } from "./services/useDictSync";
|
188
src/hooks/websocket/services/useDictSync.ts
Normal file
188
src/hooks/websocket/services/useDictSync.ts
Normal 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
18
src/main.ts
Normal 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
72
src/manifest.json
Normal 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
291
src/pages.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
5
src/pages/health/chronic/diabetes.vue
Normal file
5
src/pages/health/chronic/diabetes.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
5
src/pages/health/chronic/hypertension.vue
Normal file
5
src/pages/health/chronic/hypertension.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
1159
src/pages/health/chronic/index.vue
Normal file
1159
src/pages/health/chronic/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
532
src/pages/health/constitution/index.vue
Normal file
532
src/pages/health/constitution/index.vue
Normal 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>
|
537
src/pages/health/constitution/test.vue
Normal file
537
src/pages/health/constitution/test.vue
Normal 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>
|
5
src/pages/health/consultation/chat.vue
Normal file
5
src/pages/health/consultation/chat.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
5
src/pages/health/consultation/doctor-list.vue
Normal file
5
src/pages/health/consultation/doctor-list.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
478
src/pages/health/consultation/index.vue
Normal file
478
src/pages/health/consultation/index.vue
Normal 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>
|
5
src/pages/health/detection/detection.vue
Normal file
5
src/pages/health/detection/detection.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
612
src/pages/health/detection/index.vue
Normal file
612
src/pages/health/detection/index.vue
Normal 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>
|
5
src/pages/health/detection/report.vue
Normal file
5
src/pages/health/detection/report.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
5
src/pages/health/detection/vitals.vue
Normal file
5
src/pages/health/detection/vitals.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
454
src/pages/health/diet/index.vue
Normal file
454
src/pages/health/diet/index.vue
Normal 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>
|
505
src/pages/health/diet/record.vue
Normal file
505
src/pages/health/diet/record.vue
Normal 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>
|
945
src/pages/health/education/index.vue
Normal file
945
src/pages/health/education/index.vue
Normal 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>
|
497
src/pages/health/encyclopedia/index.vue
Normal file
497
src/pages/health/encyclopedia/index.vue
Normal 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>
|
571
src/pages/health/exercise/index.vue
Normal file
571
src/pages/health/exercise/index.vue
Normal 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>
|
5
src/pages/health/exercise/record.vue
Normal file
5
src/pages/health/exercise/record.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
589
src/pages/health/index/index.vue
Normal file
589
src/pages/health/index/index.vue
Normal 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>
|
10
src/pages/health/profile/index.vue
Normal file
10
src/pages/health/profile/index.vue
Normal 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
309
src/pages/index/index.vue
Normal 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
393
src/pages/login/index.vue
Normal 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>
|
200
src/pages/mine/about/index.vue
Normal file
200
src/pages/mine/about/index.vue
Normal 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 的项目,集成了 ESLint、Prettier、Stylelint、Husky 和
|
||||
Commitlint 等工具,确保代码规范与质量。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="item-content">
|
||||
<text class="item-label">youlai-boot</text>
|
||||
<text class="item-desc">
|
||||
基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Knife4j、Vue
|
||||
3、Element-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>
|
154
src/pages/mine/faq/index.vue
Normal file
154
src/pages/mine/faq/index.vue
Normal 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
Reference in New Issue
Block a user