Compare commits

...

29 Commits

Author SHA1 Message Date
黄思慧 43606403cb
6 months ago
黄思慧 ff15bbb610 修复了个人主页中一个显示的bug
6 months ago
黄思慧 7783d33286 Merge refs/remotes/origin/develop into refs/heads/huangsh_branch
6 months ago
hnu202326010221 dac7f96e49 Merge pull request '轮播图与知识百科管理界面实现' (#28) from liuzy_branch into develop
6 months ago
lzy ecc65315ef 轮播图与知识百科管理界面实现
6 months ago
lzy 94f8230b07 轮播图与知识百科管理界面实现
6 months ago
hnu202326010206 1038ca7436 Merge pull request '合并个人主页' (#27) from xiexy_branch into develop
6 months ago
谢欣言 0bb2ace802 个人页面和他人页面
6 months ago
黄思慧 07ffaa2213 Merge remote-tracking branch 'origin/develop' into huangsh_branch
6 months ago
黄思慧 d3e89bf3cc 初步整体美化了一下界面
6 months ago
黄思慧 d2463201dc Merge refs/remotes/origin/develop into refs/heads/huangsh_branch
6 months ago
hnu202326010221 843f5994de Merge pull request '合并文档' (#26) from huangsh_branch into develop
6 months ago
黄思慧 8612b5fcfb Merge refs/remotes/origin/develop into refs/heads/huangsh_branch
6 months ago
黄思慧 1595226bf9 初始提交:第9周文档
6 months ago
黄思慧 3f054c2024 初始提交:第9周文档
6 months ago
hnu202326010221 4a64f100de Merge pull request '增添添加家庭成员的搜索界面,将历史数据选择改为日历图标' (#25) from liuzy_branch into develop
6 months ago
lzy a507f60281 增添添加家庭成员的搜索界面,将历史数据选择改为日历图标
6 months ago
黄思慧 10d1d02321 新增web文件结构
6 months ago
hnu202326010221 60838797b4 Merge pull request '增加日期选择,批量导入,优化趋势图等等' (#24) from liuzy_branch into develop
6 months ago
lzy f81bb91be4 增加日期选择,批量导入,优化趋势图等等
6 months ago
黄思慧 82666c814b 修改json
6 months ago
黄思慧 6615d29c14 修改登录界面
6 months ago
hnu202326010221 76081494ea Merge pull request '添加血糖趋势图和历史数据的趋势图' (#23) from liuzy_branch into develop
6 months ago
lzy 031d002033 添加血糖趋势图和历史数据的趋势图
6 months ago
hnu202326010221 382ab4e9ff Merge pull request '更新智能问答、监测计划、知识百科页面' (#22) from pangqr_branch into develop
6 months ago
pqr-code 9f6d85d2ab 更新智能问答、监测计划、、知识百科页面
6 months ago
黄思慧 191a1ef20d 项目文档
6 months ago
hnu202326010221 1d3c3cb4be Merge pull request '添加历史数据切换成员功能' (#21) from liuzy_branch into develop
6 months ago
lzy 81f6e6a9d1 添加历史数据切换成员功能
6 months ago

@ -0,0 +1,25 @@
# 小组周总结-第8周
## 团队名称和起止时间
**团队名称:** 2班-Y队
**开始时间:** 2025-11-10
**结束时间:** 2025-11-16
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|------|----------|---------|-------------------------------------------------------------------------------|
| 1 | 个人主页模块的小程序界面开发 | 完成 | 谢欣言于2025-11-16前完成个人主页模块的小程序界面 |
| 2 | 糖友圈模块的小程序界面开发 | 完成 | 唐溪君于2025-11-16完成了糖友圈的小程序界面新增与删除了一些界面微调了外观细节同时解决了代码推送问题 |
| 3 | 首页、历史数据、家庭三个模块的小程序界面开发 | 完成 | 刘仲玉于2025-11-15完成了首页历史数据家庭界的小程序界面基本功能均已成功实现细节之处还需要微调校对 |
| 4 | 监测计划、智能问答、知识百科三个模块的小程序界面开发 | 完成 | 庞铨如于2025-11-16完成了监测计划、智能问答、知识百科三个模块的小程序界面重要功能ai对话交互成功实现 |
| 5 | 登录模块的小程序界面开发 | 完成 | 黄思慧于2025-11-15完成了登录模块的小程序界面开发 |
| 6 | 检查《数据库设计文档》 | 完成 | 黄思慧于2025-11-14完成了《数据库设计文档》的校验同时准备了web界面开发的统筹工作 | |
## 小结
1. **项目管理:** PM合理分配任务确保各成员工作量均衡同时完成了小程序框架设计并对组员的界面进行检查提升了团队整体效率。
2. **学习氛围浓厚:** 成员们在开发过程中积极交流技术问题,共享解决方案,形成了良好的学习与成长环境。例如代码的推送问题,小程序预览问题,代码的错误问题均通过协作沟通得到解决。
3. **团队成长与知识积累:** 通过本周的密集开发团队成员在小程序界面开发、文档规范、代码管理等方面都获得了宝贵的实践经验为后续更复杂的web界面开发做好了准备。
4. **需要获得的帮助:** 建议安排一次前后端联调的技术指导,帮助团队更好地理解协作技术细节。

@ -0,0 +1,32 @@
# 个人周总结-第8周
## 姓名和起止时间
**姓  名:** 黄思慧
**团队名称:** 2班-Y队
**开始时间:** 2025-11-10
**结束时间:** 2025-11-16
## 本周任务计划安排
| 序号 | 计划内容 | 完成情况 | 情况说明 |
|-----|-----------------------|------|-----------------------------------------------------------------------------------------------------------|
| 1 | 检查《数据库设计文档》 | 完成 | 于11月14日将唐溪君、谢欣言、刘仲玉三人撰写的《数据库设计文档》审核了一遍范式检查都满足第三范式SQL语句复制粘贴到MySQL软件中运行无误并增加了Redis、Elasticsearch数据库的设计部分 |
| 2 | 完成登录相关的界面开发 | 完成 | 于11月15日完成在微信开发者工具中完成登录相关界面的开发并已推送至远程仓库 |
| 3 | 在头歌代码仓库给出web前端源代码文件框架 | 完成 | 于11月16日部署完成web文件框架 |
| 4 | 准备第9周的个人技能考核| 未完成 | 由于第九周有计算机网络课程的期中考试本周花了太多时间在复习上把团队任务都拖延到了后面完成。且个人技能考核推迟到第11周计划第9周准备 |
## 对团队工作的建议
1. **对事不对人:** 当组员做事情出错时需要及时指出但是应该明确目的是为了解决问题、而不是责备人,所以指出错误后应该立即商量补救措施;
2. **会议要有效:** 要召开经常性的、有效的会议,简明扼要地解决问题、总结工作,切忌在会上讨论偏离会议主题的事情;
3. **认可个人和团队的成绩:** 即使有做得不完美的地方也要对组员做得好的地方适当提出嘉奖,并且肯定每个阶段团队做出的成绩。
## 小结
1. **知识储备:** 回顾了数据库设计和前端开发的相关知识,重新完善了《数据库设计文档》;
2. **工具熟悉:** 比较了WebStorm和VSCode、Subime TextWebStorm以其便捷管理远程仓库的功能、AI自动补全代码的特色、自带vue模板的亮点脱颖而出非常适合我们组用于开发web后台的界面
3. **任务推进:** 组员每次修改相应的小程序界面后都严格按照先推送到自己分支再申请合并的流程进行使得develop分支是干净的。
---

@ -0,0 +1,33 @@
# 个人周总结-第8周
## 姓名和起止时间
**姓  名:** 刘仲玉
**团队名称:** 2班-Y队
**开始时间:** 2025-11-10
**结束时间:** 2025-11-16
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- | ------------------------------------------------ | -------- | ------------------------------------------------------------ |
| 1 | 首页界面开发 | 完成 | 2025-11-15完成了首页界面开发 |
| 2 | 历史数据界面开发 | 完成 | 2025-11-15完成了历史数据界面开发不过血糖血氧心率图等等细节还需要优化 |
| 3 | 家庭界面开发 | 完成 | 2025-11-15完成了家庭界面开发不过今日家庭健康的对齐以及家庭id等问题还需要优化 |
## 对团队工作的建议
1. **定期组织技术研讨:** 针对本周遇到的共性问题,如界面布局、样式优化等,组织专题讨论。
2. **建立评审机制:** 建议在界面开发完成后组织设计评审,邀请团队成员互相检查界面细节,确保设计规范统一执行。
3. **组件共享:** 将常用的界面组件整理成团队共享资源,避免重复开发,提升开发效率。
## 小结
1. **界面开发能力提升:** 本周完成了首页、历史数据、家庭三个核心界面的开发,掌握了基础布局和组件使用。
2. **小程序开发:** 初步掌握了小程序开发工具的使用熟悉了页面路由和基础API调用。
3. **时间管理反思:** 多个界面并行开发时时间分配不够合理,后续需要更好规划开发进度。

@ -0,0 +1,33 @@
# 个人周总结-第8周
## 姓名和起止时间
**姓  名:** 庞铨如
**团队名称:** 2班-Y队
**开始时间:** 2025-11-10
**结束时间:** 2025-11-16
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- | -------------------- | -------- | ------------------------------------------------------------ |
| 1 | 监测计划模块界面开发 | 完成 | 于11月11日成功实现了计划管理主界面完成了监测方案设置表单和提醒设置组件的UI设计。开发了用于展示"餐点说明"和"测糖提示"的静态内容页面,确保用户设置计划、查看说明的流程顺畅可用 |
| 2 | 智能问答模块界面开发 | 完成 | 于11月14日完成了智能问答主界面的构建重点实现了聊天对话界面包括用户输入框与AI回复的气泡展示、"猜你想问"快捷问题区以及历史会话列表的布局与样式,模拟了完整的问答交互视觉体验 |
| 3 | 知识百科模块界面开发 | 完成 | 于11月 16日成功搭建了知识百科主界面以信息流形式展示推荐推文。完成了推文详情页的页面布局并实现了顶部搜索框及关键词搜索结果列表页的界面支持用户浏览和搜索知识内容 |
| 4 | 微信小程序开发巩固 | 完成 | 在开发过程中,通过实践深入掌握了微信小程序的模板与样式编写、数据绑定以及页面路由传参等核心技能,提升了开发效率 |
## 对团队工作的建议
1. **UI/UX设计规范** 建议建立统一的微信小程序UI设计规范包括色彩体系、组件样式、交互反馈等确保各模块界面风格一致
2. **组件复用机制:** 制定公共组件开发标准将常用UI组件如按钮、输入框、列表项等封装为可复用组件提高开发效率
3. **代码审查制度:** 建立前端代码审查机制重点关注WXML结构合理性、WXSS样式规范性和JavaScript逻辑严谨性
4. **技术分享安排:** 建议定期组织微信小程序开发经验分享会,交流各模块开发中遇到的典型问题及解决方案,特别是性能优化和用户体验提升方面。
## 小结
1. **界面开发成果:** 本周按计划完成了监测计划、智能问答、知识百科三个核心功能模块的界面开发实现了需求文档中要求的主要UI界面为后续功能逻辑开发奠定了良好的基础。
2. **技术能力提升:** 通过实际开发过程熟练掌握了WXML数据渲染、WXSS样式编写及JavaScript页面逻辑处理对微信开发者工具的各项功能有了更深入的了解开发效率明显提升。
3. **问题解决经验:** 在开发过程中遇到的技术难点,如组件交互、固定组件等,通过查阅官方文档和实验验证得到了有效解决,增强了独立解决问题的能力。
4. **用户体验思考:** 在界面开发过程中开始注重用户交互体验的细节设计如页面跳转流畅性、操作反馈及时性等对UI/UX设计有了更深刻的理解。
5. **希望获得的帮助:** 希望老师能组织关于**微信小程序最佳UI/UX实践**的分享或提供相关资料,帮助我们进一步完善界面设计,提升产品的用户体验质量。

@ -0,0 +1,39 @@
# 个人周总结-第8周
## 姓名和起止时间
**姓  名:** 唐溪君
**团队名称:** 2班-Y队
**开始时间:** 2025-11-10
**结束时间:** 2025-11-16
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ----|----------------------|------|-------------------------------------------------------------------------------------------------|
| 1 | 学习优秀小程序的设计风格 | 完成 | 搜索了其他有关健康管理的小程序并学习了它们的界面设计同时仔细研究了微信朋友圈、QQ空间等可以供我参考设计糖友圈的相关界面例如它们图片呈现的尺寸和布局。 |
| 2 | 掌握核心技术 | 完成 | 我已基本掌握微信小程序的核心开发技术栈包括结构与逻辑分离的WXML、样式控制语言WXSS、实现交互功能的JavaScript以及负责配置的JSON同时使用真机调试帮助进一步的界面美化。 |
| 3 |修改糖友圈相关的小程序界面| 完成 | 截至2025-11-16在pm给定的框架上新增了三个界面将原有的界面多余的功能删除并且在与pm的讨论下修改了部分影响美观的细节等后续再全局地微调即可。
| 4 |协同组员完成其他界面设计| 完成 | 小程序界面代码撰写部分的工作没有出现组员难以完成的情况,都是各自完成了开始分配的任务,但是帮助了个别组员解决从仓库拉取和推送时出现的问题。
## 对团队工作的建议
1. **分享方法,共同解决问题:** 有些大家都需要进行的步骤可以随时在群里分享解决某些问题的方法,比如我和刘仲玉同学在拉取和推送环节都遇到了相同的问题,我率先解决之后分享给他;在开始真机调试时有文件大小限
制问题且AI并没有给出好的解决方法后来是刘仲玉同学告诉我们修改微信开发者工具的设置即可。这样大大提高了我们的工作效率。
## 小结
1. **界面设计与开发:** 我深入研究了优秀小程序特别是健康类及社交类产品的设计风格并以此为指导在项目框架内新增了三个界面同时对原有界面进行了功能精简与细节优化。通过与PM的积极沟通确保了界面美观性与产品需求的一致性。
2. **技术学习与应用:** 我已系统掌握微信小程序开发的核心技术栈WXML/WXSS/JavaScript/JSON并能熟练运用真机调试功能进行界面美化与问题排查有效提升了开发效率与页面质量。
3. **团队协作与互助:** 在完成自身任务的同时我主动协助组员解决了Git代码拉取与推送过程中遇到的技术问题。我们团队形成了良好的互助氛围能够及时在群内共享解决特定问题的方法如真机调试的文件大小限制问题共同扫清了开发障碍。
4. **个人感悟:** 本周我经历了从界面设计到前端开发的完整流程。通过独立完成多个页面的构建与优化将WXML/WXSS等技术应用于实战觉得完成前端界面其实是一个很繁琐的过程借助AI的确可以提升我们界面的精致度。
在协助组员解决Git问题并共享调试技巧的过程中也意识到互相帮助的确可以大大提升工作效率希望之后的学习中大家都可以像这样一起解决问题。
---
## 【注】
1. 本周的任务都是持续性在做,给出不了太具体的时间。

@ -0,0 +1,33 @@
# 个人周总结-第8周
## 姓名和起止时间
**姓  名:** 谢欣言
**团队名称:** 2班-Y队
**开始时间:** 2025-11-10
**结束时间:** 2025-11-16
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|----------------------|------|-----------------------------------------------|
| 1 | 学习微信开发者小程序代码编写 | 完成 | 通过网上资料,了解了微信小程序开发的代码编写规范,学会了使用大模型辅助开发 |
| 2 | 学习其他小程序页面 | 完成 | 在空闲时间里查找了市面上优秀的个人页面设计,学习其设计风格,并运用在了前端页面设计上 |
| 3 | 学习后台页面代码编写 | 完成 | 通过网上资料,学习后台页面的开发,为下一周编写后台页面代码做准备 |
| 4 | 个人主页模块的小程序界面 | 完成 | 于2025-11-16日在微信开发者工具上完成个人主页模块的界面开发 |
## 对团队工作的建议
1. **团队协作:** 在前几周的磨合下,小组的协作能力在增强,可以进一步调动组员的积极性,小组成员之间相互检查监督,保证工作的稳步进行。
2. **沟通与协调:** 可以调动小组成员的积极性,成员积极分享自己的想法,协调成员的不同观点,在不断地沟通中,找出最佳解决方案。
## 小结
1. **代码编写:** 本周完成了微信小程序个人模块的代码编写,在与大模型协作完成代码编写的过程中,对小程序的代码有了进一步的了解,纠正了一些误区。
2. **交流沟通:** 本周的代码编写工作看似独立,其实同样需要组员之间交流遇到的问题,及时合并分支,对各个模块的重复功能进行复用。
3. **项目管理:** 在各个组员编写各自负责模块代码之前PM先定下了微信小程序项目框架使得各个模块之间相互独立便于合并。
4. **需要的帮助:** 希望可以提供小程序代码框架的介绍和讲解。
5. **个人感悟:** 本周开始编写代码,在实际编写代码过程中遇到了很多困难,有的是依靠大模型解决,有的是需要自己分析代码结构来解决。在实践过程中,对代码编写有了更深的理解。
---

@ -0,0 +1,82 @@
# 小组会议纪要-第9周
## 会议记录概要
**团队名称:** 2班-Y队<br>
**指导老师:** 周军海 <br>
**主 持 人:** 黄思慧<br>
**记录人员:** 庞铨如 <br>
**会议主题:** 任务安排<br>
**会议地点:** 天马二食堂二楼<br>
**会议时间:** 2025-11-16 19:00-20:00<br>
**记录时间:** 2025-11-16 23:00 <br>
**参与人员:** 黄思慧、唐溪君、谢欣言、庞铨如、刘仲玉
---
## 会议内容
### 开发工具统一确认
为确保团队开发效率和质量,会议确认了以下开发工具:
1**Web界面开发工具** 统一使用WebStorm进行Web界面开发
2**版本控制:** 继续使用Git进行代码版本管理
3**数据库环境:** 使用MySQL作为数据库管理系统并使用Redis数据库缓存聊天记录等需要多次读取数据库的内容、使用Elasticsearch数据库辅助糖友圈搜索、糖友搜索、知识百科搜索、智能问答聊天记录搜索的功能
### 第九周任务详细分工
#### 具体任务安排如下:
| 执行人 | 开发任务 | 具体内容 |
| ------ | -------------------------------------------------- | ------------------------------------------------------------ |
| 唐溪君 | 管理糖友圈的Web界面 | 负责糖友圈相关功能的Web界面开发与维护 |
| 谢欣言 | 管理客户、管理系统web界面、MySQL数据库环境搭建 | 负责客户管理和系统管理的Web界面开发完成MySQL数据库环境搭建包括创建数据库、创建表 |
| 刘仲玉 | 管理首页推送、管理知识百科的web界面 | 负责首页内容推送和知识百科模块的Web界面开发 |
| 庞铨如 | 管理用户设备的web界面、UML活动图 | 负责用户设备管理相关的Web界面开发完成系统UML活动图设计 |
| 黄思慧 | 部署后端代码结构、部署云服务器、登录相关的界面开发 | 负责后端代码结构部署和云服务器环境配置,完成登录相关的界面开发 |
### 开发规范要求
1**代码规范:** 要求所有成员遵循统一的代码编写规范;
2**进度同步:** 每日在群内汇报开发进度和遇到的问题;
3**文档更新:** 开发过程中及时更新相关技术文档;
------
## 问题总结
### 已解决问题:
1. 明确了本周各项开发任务的具体分工;
2. 确定了统一的Web开发工具为WebStorm
3. 建立了清晰的任务责任体系;
### 待解决问题:
1. 数据库设计与前端界面数据对接的具体实现方案;
2. 各模块间接口定义和通信协议的统一;
3. 云服务器环境配置的具体技术要求;
4. 由于本周《计算机网络》课程期中考试与界面开发工作量较大,部分成员的原定任务尚有少量收尾工作未完成,需在本周初尽快完成。
---
## 小组协作情况总结
1. **协作情况:** 任务分配合理,各成员对各自职责认识清晰,沟通顺畅;
2. **进度预期:** 本周为关键开发阶段,预计可按计划完成各自任务;
## 一周纪律情况总结
1. **纪律情况:** 全体成员按时参加会议,无缺席情况,团队纪律良好;
---
## 【注】
1. 开发过程中注意代码质量和可维护性;
2. 遇到技术难题及时在群内讨论解决;
3. 按时完成各自任务,确保项目整体进度。

@ -0,0 +1,34 @@
# 小组周计划-第9周
## 团队名称和起止时间
**团队名称:** 2班-Y队
**开始时间:** 2025-11-17
**结束时间:** 2025-11-23
## 本周任务计划安排
| 序号 | 计划内容 | 执行人 | 情况说明 |
|----|---------------------|-----|-----------------------------------------------------------|
| 1 | 客户管理、系统管理模块的web界面 | 谢欣言 | 2025-11-23前在WebStorm开发工具上完成客户管理、系统管理模块的界面开发要求跳转逻辑完整清晰 |
| 2 | 糖友圈模块的web界面 | 唐溪君 | 2025-11-23前在WebStorm开发工具上完成糖友圈模块的界面开发要求跳转逻辑完整清晰 |
| 3 | 首页推送、知识百科管理模块的web界面 | 刘仲玉 | 2025-11-23前在WebStorm开发工具上完成首页推送、知识百科管理模块的界面开发要求跳转逻辑完整清晰 |
| 4 | 设备管理模块的小程序界面 | 庞铨如 | 2025-11-23前在WebStorm开发工具上完成用户设备管理三个模块的界面开发要求跳转逻辑完整清晰 |
| 5 | 登录模块的web界面 | 黄思慧 | 2025-11-23前在WebStorm开发工具上完成登录模块的界面开发要求跳转逻辑完整清晰 |
| 6 | 云服务器部署 | 黄思慧 | 2025-11-23前将后端源代码初步部署到云服务器上目标是熟悉部署流程 |
| 7 | MySQL数据库环境搭建 | 谢欣言 | 2025-11-23前在MySQL软件中创建数据库并且创建好相关的表。 |
| 8 | 绘制UML活动图 | 庞铨如 | 2025-11-23前规范完成UML活动图的绘制为后端开发明确事件流的时间线 |
## 小结
1. **沟通协作:** 组员间要增强技术上的交流,提高团队的开发效率。
2. **前端开发:** 认真完成前端开发,为后端开发打好基础。
---
## 【注】
1. WebStrom的推送和拉取和微信开发者工具略有差别需要事先查阅经验帖再进行推送和拉取
2. WebStorm中没有实时预览窗口每次运行是在程序内置终端中依次输入“npm run build”、“npm run dev”指令运行成功后点击输出的网址才可以看到效果
3. 每一个表格都要复用chat文件夹里的表格以统一整个web前端的风格
4. 数据库的名称为GlucowiseBase创建数据库后直接将数据库设计文档中的创建表的语句复制粘贴到软件中即可《数据库设计文档》以仓库中的版本为准

@ -0,0 +1,26 @@
# 个人周计划-第9周
## 姓名和起止时间
**姓  名:** 黄思慧
**团队名称:** 2班-Y队
**开始时间:** 2025-11-17
**结束时间:** 2025-11-23
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
|-----|----------------------|-----|-----------------------------------------------------------------|
| 1 | 完成登录相关的界面开发 | 个人 | 于11月23日前在WebStorm开发工具中完成登录相关界面的开发要在原有墨刀原型的基础上美化一下布局并及时推送至远程仓库 |
| 2 | 在头歌代码仓库给出服务器端源代码文件框架 | 个人 | 于11月23日前部署完成后端源代码文件框架使文件管理井井有条、后续增删改查都更加高效 |
| 3 | 回顾UML设计要点 | 个人 | 于11月20日前回顾UML用例图设计要点和规范目标是能准确把握用例图的粒度和用例之间的关系 |
| 4 | 部署云服务器 | 个人 | 在完成源代码文件框架后、11月23日前完成云服务器部署让团队有更多的时间发现部署到云端和在本地的差别及时补上缺漏 |
## 小结
1. **知识储备:** 回顾web前端开发和UML设计的相关知识、学习云服务器的相关内容
2. **工具熟悉:** 熟练使用WebStorm开发工具高效完成界面开发
3. **任务推进:** 部署好后端代码框架为第10周的正式后端开发做好准备。
---

@ -0,0 +1,26 @@
# 个人周计划-第9周
## 姓名和起止时间
**姓  名:** 刘仲玉
**团队名称:** 2班-Y队
**开始时间:** 2025-11-17
**结束时间:** 2025-11-23
## 本周任务计划安排
| 序号 | 计划内容| 协作人 | 情况说明 |
| ----| ------ | ------| ------- |
| 1 | 下载安装webstorm | 个人 | 周内需要使用webstorm工具进行web界面开发 |
| 2 | 管理首页推送的web界面 | 个人 | 周内完成开发管理首页推送的web界面 |
| 3 | 管理知识百科的web界面 | 个人 | 周内完成开发管理知识百科的web界面 |
| 4 | 优化上周负责的微信小程序界面 | 个人 | 上周负责的小程序界面还有些细节没有完善好,在完成这周任务的前提下可以进行修改优化 |
## 小结
1. **技术学习与工具应用:** 本周计划学习并熟练使用WebStorm进行Web界面开发提升开发效率和代码质量。同时探索其与微信开发者工具的协同使用方式。
2. **时间管理与任务规划:** 在完成本周主要任务的前提下,合理分配时间用于优化上周的小程序界面,确保任务进度与质量并重。
3. **协作思考:** 团队在界面风格统一上可以加强沟通,建立设计规范,以保持整体设计的一致性。

@ -0,0 +1,24 @@
# 个人周计划-第9周
## 姓名和起止时间
**姓  名:** 庞铨如
**团队名称:** 2班-Y队
**开始时间:** 2025-11-17
**结束时间:** 2025-11-23
## 本周任务计划安排
| 序号 | 计划内容| 协作人 | 情况说明 |
| ----| ------ | ------| ------- |
| 1 | WebStorm开发工具学习与应用 | 个人 | 下载并学习WebStorm开发环境的基本配置与使用方法掌握代码编辑、调试、版本控制集成等核心功能为Web界面开发做好技术准备 |
| 2 | 优秀Web界面设计研究 | 个人 | 研究分析同类优秀的设备管理Web界面设计学习其布局风格、交互逻辑和用户体验设计特点为后续界面开发积累设计灵感和最佳实践 |
| 3 | UML活动图标准学习与绘制 | 个人 | 系统学习UML活动图的标准画法、符号含义和使用规范掌握活动图在业务流程建模中的应用方法为系统设计提供清晰的流程可视化 |
| 4 | 用户设备管理Web界面开发 | 个人 | 使用WebStorm开发工具基于用例文档和墨刀原型实现用户设备管理的Web界面 |
## 小结
1. **技术准备:** 本周将重点完成WebStorm开发工具的学习和环境搭建为后续Web界面开发提供稳定的技术支撑
2. **设计研究:** 通过分析优秀Web界面案例建立对设备管理界面设计的深入理解确保开发出的界面既美观又实用
3. **实践开发:** 结合工具使用和设计研究动手实现用户设备管理的Web界面将理论知识转化为实际开发能力
4. **期望帮助:** 希望老师能提供一些UML活动图绘制的典型案例分析或推荐相关的学习资源以帮助我们更好地掌握这一建模工具。

@ -0,0 +1,32 @@
# 个人周计划-第9周
## 姓名和起止时间
**姓  名:** 唐溪君
**团队名称:** 2班-Y队
**开始时间:** 2025-11-17
**结束时间:** 2025-11-23
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
|-----|----------------|-----|------------------------------------------------------------|
| 1 | 学习优秀web界面的设计风格 | 个人 | 观察主流健康类Web产品的界面设计重点分析其布局、配色与交互模式为web界面设计提供参考依据。 |
| 2 | 掌握核心技术 | 个人 | 在做中学系统掌握Web前端开发核心技术HTML/CSS/JS。 |
| 3 | 完成管理糖友圈的web界面 | 个人 | 在2025-11-23前参照之前的原型和pm将提供的框架在webstorm开发工具完成管理糖友圈的web界面的开发。 |
| 4 | 美观上完善小程序界面设计 | 个人 | 在2025-11-23前的空闲时间浏览全部界面对负责部分的组件进行精细化打磨重点优化间距、字体与图标细节 |
## 小结
1. **设计规范深化:** 本周需重点研究健康管理类Web端产品的设计通过拆解优秀案例来掌握后台系统的布局逻辑与交互范式希望获得老师在Web端信息架构与导航设计方面的指导。
2. **开发技能实践:** 为顺利实现Web界面需在实战中巩固HTML、CSS与JavaScript核心技术重点提升根据原型进行界面构建的能力并期望与组员在CSS布局与组件封装等具体问题上交流经验。
3. **开发任务协作:** 在完成管理糖友圈Web核心界面开发的同时需主动关注团队整体进度在组件规范和代码风格上与组员保持统一确保项目模块间的协调性共同应对技术挑战。
4. **开发流程熟悉:** 已开始熟悉Web前端开发环境与构建工具希望在代码版本管理、浏览器调试技巧等方面进行实践并与团队一同建立高效的协作开发流程。
---
## 【注】
无需要强调的注解。

@ -0,0 +1,27 @@
# 个人周计划-第9周
## 姓名和起止时间
**姓  名:** 谢欣言
**团队名称:** 2班-Y队
**开始时间:** 2025-11-17
**结束时间:** 2025-11-23
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
|----|------------------|-----|-------------------------------------------|
| 1 | web页面代码知识学习 | 个人 | 通过网上资料了解web页面的代码编写规范了解基本框架和语法 |
| 2 | 管理客户、管理系统web页面编写 | 个人 | 在2025-11-23前参考原型页面完成管理客户、管理系统web页面编写和优化 |
| 3 | MySQL数据库环境搭建知识学习 | 个人 | 通过网上资料学习MySQL数据库环境搭建知识 |
| 4 | MySQL数据库环境搭建 | 个人 | 在2025-11-23前完成数据库环境的搭建包括创建数据库、创建表 |
## 小结
1. **知识储备:** 系统学习web页面代码知识以及MySQL数据库环境搭建知识了解web页面代码的框架结构字段含义
2. **学习需求:** 掌握前端三件套HTML+CSS+JS基础开发能力理解数据库设计原则和SQL基本操作具备独立完成简单管理系统的开发能力
3. **工具使用:** 在网络上查找相关资料,准备好数据库搭建的相关资源,如开发工具,数据库工具以及学习资料;
4. **需要获得的帮助:** 希望可以得到数据库设计的业务逻辑指导,代码审查和一些优化方面的建议。
---

@ -13,7 +13,16 @@
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
"minifyWXML": true,
"compileWorklet": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"localPlugins": false,
"disableUseStrict": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
@ -22,6 +31,5 @@
"include": []
},
"appid": "wxce6ab13cde21f6da",
"miniprogramRoot": "src/wx_app/",
"editorSetting": {}
}

@ -1,27 +1,22 @@
{
"miniprogramRoot": "src/wx_app/",
"libVersion": "3.11.2",
"projectname": "GlucoWise",
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxce6ab13cde21f6da",
"miniprogramRoot": "src/wx_app/",
"editorSetting": {}
"urlCheck": true,
"coverView": false,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": false,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
}
}

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,20 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"serve": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.24",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.2.2"
}
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,7 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

@ -0,0 +1,101 @@
<script setup>
import SidebarNav from './SidebarNav.vue'
const props = defineProps({
title: {
type: String,
required: true,
},
})
const profile = {
name: '张三',
avatar:
'https://images.unsplash.com/photo-1504595403659-9088ce801e29?auto=format&fit=crop&w=200&q=80',
}
</script>
<template>
<div class="page">
<SidebarNav />
<div class="content">
<header class="topbar">
<div class="page-title">{{ props.title }}</div>
<div class="user">
<img class="avatar" :src="profile.avatar" alt="avatar" />
<span class="name">{{ profile.name }}</span>
</div>
</header>
<main class="main-area">
<slot />
</main>
</div>
</div>
</template>
<style scoped>
.page {
display: grid;
grid-template-columns: auto 1fr;
min-height: 100vh;
background: #f2f6fb;
}
.content {
display: flex;
flex-direction: column;
}
.topbar {
height: 62px;
padding: 0 24px;
background: #428cf9;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 6px 16px rgba(66, 140, 249, 0.2);
}
.page-title {
font-size: 17px;
font-weight: 600;
}
.user {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.6);
}
.name {
font-weight: 600;
}
.main-area {
padding: 20px 24px 32px;
display: flex;
flex-direction: column;
gap: 18px;
}
@media (max-width: 900px) {
.page {
grid-template-columns: 1fr;
}
.topbar {
position: sticky;
top: 0;
z-index: 2;
}
}
</style>

@ -0,0 +1,74 @@
<script setup>
import { RouterLink } from 'vue-router'
const menuItems = [
{ icon: '🧭', label: '个人主页', path: '/home' },
{ icon: '🖼️', label: '首页轮播图管理', path: '/banners' },
{ icon: '💬', label: '糖友圈发布管理', path: '/circle/publish' },
{ icon: '🚨', label: '糖友圈举报管理', path: '/circle/reports' },
{ icon: '⛔', label: '糖友圈恶意用户', path: '/circle/malicious' },
{ icon: '🤖', label: '糖友圈智能审核设置', path: '/circle/auto-review' },
{ icon: '📚', label: '知识百科管理', path: '/knowledge' },
{ icon: '👥', label: '客户管理', path: '/clients' },
{ icon: '⚙️', label: '系统管理', path: '/system' },
{ icon: '🔧', label: '设备管理', path: '/devices' },
]
</script>
<template>
<aside class="sidebar">
<div class="brand">智慧糖管理系统</div>
<nav class="menu">
<RouterLink v-for="item in menuItems" :key="item.label" class="menu-item" :to="item.path">
<span class="icon">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</RouterLink>
</nav>
</aside>
</template>
<style scoped>
.sidebar {
width: 220px;
background: linear-gradient(180deg, #3380ff 0%, #2c63f4 100%);
color: #fff;
padding: 22px 0;
display: flex;
flex-direction: column;
}
.brand {
font-weight: 700;
font-size: 18px;
padding: 0 24px 20px 24px;
}
.menu {
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 12px;
}
.menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 10px;
color: #dfe9ff;
transition: background 0.2s ease, color 0.2s ease;
}
.menu-item:hover,
.menu-item.router-link-active {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.icon {
width: 20px;
display: inline-flex;
justify-content: center;
}
</style>

@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'
createApp(App).use(router).mount('#app')

@ -0,0 +1,35 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import LoginView from '../views/LoginView.vue'
import DashboardView from '../views/DashboardView.vue'
import BannerView from '../views/BannerView.vue'
import KnowledgeView from '../views/KnowledgeView.vue'
import ClientsView from '../views/ClientsView.vue'
import SystemView from '../views/SystemView.vue'
import DevicesView from '../views/DevicesView.vue'
import CirclePublishView from '../views/CirclePublishView.vue'
import CircleReportView from '../views/CircleReportView.vue'
import CircleMaliciousView from '../views/CircleMaliciousView.vue'
import CircleAutoReviewView from '../views/CircleAutoReviewView.vue'
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', name: 'login', component: LoginView },
{ path: '/home', name: 'home', component: DashboardView },
{ path: '/banners', name: 'banners', component: BannerView },
{ path: '/knowledge', name: 'knowledge', component: KnowledgeView },
{ path: '/clients', name: 'clients', component: ClientsView },
{ path: '/system', name: 'system', component: SystemView },
{ path: '/devices', name: 'devices', component: DevicesView },
{ path: '/circle/publish', name: 'circlePublish', component: CirclePublishView },
{ path: '/circle/reports', name: 'circleReports', component: CircleReportView },
{ path: '/circle/malicious', name: 'circleMalicious', component: CircleMaliciousView },
{ path: '/circle/auto-review', name: 'circleAutoReview', component: CircleAutoReviewView },
]
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior: () => ({ top: 0 }),
})
export default router

@ -0,0 +1,954 @@
:root {
/* 字体系统 */
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', system-ui, -apple-system, sans-serif;
line-height: 1.6;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 色彩系统 - 完整的调色板 */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
--color-success-50: #ecfdf5;
--color-success-100: #d1fae5;
--color-success-500: #10b981;
--color-success-600: #059669;
--color-success-700: #047857;
--color-warning-50: #fffbeb;
--color-warning-100: #fef3c7;
--color-warning-500: #f59e0b;
--color-warning-600: #d97706;
--color-warning-700: #b45309;
--color-error-50: #fef2f2;
--color-error-100: #fee2e2;
--color-error-500: #ef4444;
--color-error-600: #dc2626;
--color-error-700: #b91c1c;
--color-gray-50: #f8fafc;
--color-gray-100: #f1f5f9;
--color-gray-200: #e2e8f0;
--color-gray-300: #cbd5e1;
--color-gray-400: #94a3b8;
--color-gray-500: #64748b;
--color-gray-600: #475569;
--color-gray-700: #334155;
--color-gray-800: #1e293b;
--color-gray-900: #0f172a;
/* 字体大小系统 */
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.25rem; /* 20px */
--text-2xl: 1.5rem; /* 24px */
--text-3xl: 1.875rem; /* 30px */
/* 行高系统 */
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* 间距系统 */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
/* 阴影系统 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* 圆角系统 */
--radius-sm: 0.25rem; /* 4px */
--radius: 0.375rem; /* 6px */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 0.75rem; /* 12px */
--radius-xl: 1rem; /* 16px */
--radius-2xl: 1.5rem; /* 24px */
}
*,
*::before,
*::after {
box-sizing: border-box;
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
body {
margin: 0;
min-height: 100vh;
background: var(--color-gray-50);
color: var(--color-gray-800);
font-size: var(--text-base);
line-height: var(--line-height-normal);
}
a {
color: inherit;
text-decoration: none;
}
button {
font: inherit;
cursor: pointer;
border: none;
outline: none;
}
/* 固定侧边菜单栏 */
.layout-sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 260px;
background: #fff;
box-shadow: var(--shadow-lg);
z-index: 1000;
overflow-y: auto;
transition: transform 0.3s ease;
}
.layout-main {
margin-left: 260px;
min-height: 100vh;
transition: margin-left 0.3s ease;
}
/* 移动端侧边栏隐藏 */
@media (max-width: 1024px) {
.layout-sidebar {
transform: translateX(-100%);
}
.layout-sidebar.open {
transform: translateX(0);
}
.layout-main {
margin-left: 0;
}
}
/* 侧边栏滚动条样式 */
.layout-sidebar::-webkit-scrollbar {
width: 4px;
}
.layout-sidebar::-webkit-scrollbar-track {
background: var(--color-gray-100);
}
.layout-sidebar::-webkit-scrollbar-thumb {
background: var(--color-gray-400);
border-radius: var(--radius);
}
.layout-sidebar::-webkit-scrollbar-thumb:hover {
background: var(--color-gray-500);
}
/* 卡片样式 - 增强版 */
.app-card {
background: #fff;
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
border: 1px solid var(--color-gray-200);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.app-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
/* 面板头部 - 增强版 */
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-6) var(--space-6) var(--space-4);
border-bottom: 1px solid var(--color-gray-100);
background: linear-gradient(to bottom, #fff, var(--color-gray-50));
}
.panel-title {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-gray-900);
letter-spacing: -0.025em;
line-height: var(--line-height-tight);
margin: 0;
}
/* 统一统计卡片字体大小 */
.cards-grid .stat-card .stat-value {
font-size: 2.25rem !important;
font-weight: 800;
color: var(--color-gray-900);
line-height: 1;
letter-spacing: -0.025em;
}
.cards-grid .stat-card .muted {
color: var(--color-gray-500);
font-size: var(--text-sm) !important;
font-weight: 500;
}
.cards-grid .stat-card .stat-trend {
font-size: var(--text-xs) !important;
font-weight: 600;
margin-top: var(--space-1);
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
/* 分段控制器 - 增强版 */
.segmented {
display: inline-flex;
background: var(--color-gray-100);
border-radius: var(--radius-lg);
padding: var(--space-1);
gap: var(--space-1);
}
.segmented button {
border: none;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
background: transparent;
color: var(--color-gray-600);
cursor: pointer;
font-weight: 600;
font-size: var(--text-sm);
transition: all 0.2s ease;
position: relative;
}
.segmented .active {
background: #fff;
color: var(--color-primary-600);
box-shadow: var(--shadow-sm);
}
/* 分页 - 增强版 */
.pagination {
display: flex;
gap: var(--space-2);
align-items: center;
justify-content: center;
padding: var(--space-6) 0;
}
.pagination button {
width: 2.5rem;
height: 2.5rem;
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-md);
background: #fff;
cursor: pointer;
color: var(--color-gray-600);
font-weight: 600;
font-size: var(--text-sm);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.pagination .active {
background: var(--color-primary-600);
color: #fff;
border-color: var(--color-primary-600);
}
.pagination button:hover:not(.active) {
border-color: var(--color-primary-500);
color: var(--color-primary-600);
}
/* 搜索行 */
.search-row {
display: flex;
gap: var(--space-3);
align-items: center;
flex-wrap: wrap;
}
/* 选择框 - 增强版 */
.select {
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-lg);
padding: var(--space-3) var(--space-4);
background: #fff;
min-width: 8rem;
font-size: var(--text-sm);
color: var(--color-gray-700);
transition: all 0.2s ease;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2364748b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.select:focus {
outline: none;
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* 表格样式 - 增强版 */
.table-container {
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--color-gray-200);
background: #fff;
}
.table {
width: 100%;
border-collapse: collapse;
background: #fff;
font-size: var(--text-sm);
line-height: var(--line-height-relaxed);
}
.table th,
.table td {
padding: var(--space-4) var(--space-4);
border-bottom: 1px solid var(--color-gray-100);
text-align: left;
color: var(--color-gray-700);
}
.table th {
color: var(--color-gray-600);
font-weight: 600;
background: var(--color-gray-50);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--color-gray-200);
}
.table tbody tr {
transition: background-color 0.15s ease;
}
.table tbody tr:hover {
background-color: var(--color-primary-50);
}
.table tbody tr:last-child td {
border-bottom: none;
}
/* 标签样式 - 增强版 */
.tag {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1;
border: 1px solid transparent;
}
.tag.blue {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
border-color: rgba(59, 130, 246, 0.2);
}
.tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
/* 工具栏 - 增强版 */
.toolbar {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
padding: var(--space-5);
background: var(--color-gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
margin: var(--space-5);
}
/* 输入框 - 增强版 */
.input {
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-lg);
padding: var(--space-3) var(--space-4);
min-width: 12rem;
font-size: var(--text-sm);
background: #fff;
color: var(--color-gray-700);
transition: all 0.2s ease;
flex: 1;
}
.input:focus {
outline: none;
border-color: var(--color-primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input::placeholder {
color: var(--color-gray-400);
}
/* 按钮样式 - 增强版 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
border: none;
border-radius: var(--radius-lg);
padding: var(--space-3) var(--space-5);
font-weight: 600;
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.2s ease;
line-height: 1;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn.primary {
background: linear-gradient(135deg, var(--color-primary-500) 0%, var(--color-primary-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.primary:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn.primary:active {
transform: translateY(0);
}
.btn.secondary {
background: #fff;
color: var(--color-gray-700);
border: 1px solid var(--color-gray-300);
box-shadow: var(--shadow-sm);
}
.btn.secondary:hover {
background: var(--color-gray-50);
border-color: var(--color-gray-400);
transform: translateY(-1px);
}
.btn.small {
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
border-radius: var(--radius-md);
}
/* 统计卡片网格 - 增强版 */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: var(--space-5);
margin: var(--space-5);
}
.stat-card {
height: 150px;
padding: var(--space-5);
border-radius: var(--radius-xl);
background: #fff;
box-shadow: var(--shadow);
border: 1px solid var(--color-gray-200);
display: flex;
flex-direction: column;
gap: var(--space-2);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--color-primary-500);
}
.stat-card.warning::before {
background: var(--color-warning-500);
}
.stat-card.danger::before {
background: var(--color-error-500);
}
.stat-card.success::before {
background: var(--color-success-500);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.stat-trend.positive {
color: var(--color-success-600);
}
.stat-trend.negative {
color: var(--color-error-600);
}
.stat-trend.warning {
color: var(--color-warning-600);
}
/* 链接按钮 - 增强版 */
.link-btn {
color: var(--color-primary-600);
font-weight: 600;
font-size: var(--text-sm);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius);
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.link-btn:hover {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
}
/* 表格底部 */
.table-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-5);
border-top: 1px solid var(--color-gray-200);
background: var(--color-gray-50);
}
.pagination-info {
color: var(--color-gray-500);
font-size: var(--text-sm);
font-weight: 500;
}
/* 加载状态 */
.loading {
position: relative;
overflow: hidden;
}
.loading::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
100% { transform: translateX(100%); }
}
/* 响应式设计 - 增强版 */
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: var(--space-3);
margin: var(--space-4);
}
.input, .select {
min-width: auto;
flex: 1;
}
.cards-grid {
grid-template-columns: 1fr;
margin: var(--space-4);
gap: var(--space-4);
}
.table {
font-size: var(--text-xs);
}
.table th,
.table td {
padding: var(--space-3) var(--space-3);
}
.panel-header {
flex-direction: column;
gap: var(--space-4);
align-items: stretch;
padding: var(--space-4);
}
.table-footer {
flex-direction: column;
gap: var(--space-4);
text-align: center;
}
.pagination {
flex-wrap: wrap;
}
}
/* 移动端卡片式表格 */
@media (max-width: 640px) {
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-mobile-card {
display: block;
}
.table-mobile-card tr {
display: block;
margin-bottom: var(--space-4);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
background: #fff;
box-shadow: var(--shadow-sm);
}
.table-mobile-card td {
display: block;
border: none;
padding: var(--space-3);
border-bottom: 1px solid var(--color-gray-100);
}
.table-mobile-card td:last-child {
border-bottom: none;
}
.table-mobile-card td::before {
content: attr(data-label);
font-weight: 600;
color: var(--color-gray-600);
display: block;
font-size: var(--text-xs);
margin-bottom: var(--space-1);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
/* 可访问性改进 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.btn:focus,
.input:focus,
.select:focus {
outline: 2px solid var(--color-primary-300);
outline-offset: 2px;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-gray-100);
border-radius: var(--radius);
}
::-webkit-scrollbar-thumb {
background: var(--color-gray-400);
border-radius: var(--radius);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-gray-500);
}
/* 在现有的样式基础上添加以下响应式代码 */
/* 超大屏幕 */
@media (min-width: 1536px) {
.container {
max-width: 1280px;
margin: 0 auto;
}
}
/* 大屏幕 */
@media (max-width: 1280px) {
.cards-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* 中等屏幕 */
@media (max-width: 1024px) {
.cards-grid {
grid-template-columns: repeat(2, 1fr);
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar > * {
width: 100%;
}
.search-group {
flex-direction: column;
}
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
/* 平板 */
@media (max-width: 768px) {
.cards-grid {
grid-template-columns: 1fr;
}
.panel-header {
flex-direction: column;
gap: var(--space-4);
align-items: stretch;
}
.table th,
.table td {
padding: var(--space-3);
font-size: var(--text-xs);
}
.btn {
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
}
.input,
.select {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
}
}
/* 手机 */
@media (max-width: 640px) {
.app-card {
margin: var(--space-2);
padding: var(--space-3);
}
.table-footer {
flex-direction: column;
gap: var(--space-3);
text-align: center;
}
.pagination {
flex-wrap: wrap;
justify-content: center;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
}
/* 小手机 */
@media (max-width: 480px) {
.app-card {
margin: var(--space-1);
padding: var(--space-2);
border-radius: var(--radius-lg);
}
.toolbar {
padding: var(--space-3);
gap: var(--space-2);
}
.stat-card {
padding: var(--space-3);
}
.table th,
.table td {
padding: var(--space-2);
font-size: var(--text-xs);
}
}
/* 移动端表格卡片式布局 */
@media (max-width: 768px) {
.table-mobile {
display: block;
}
.table-mobile thead {
display: none;
}
.table-mobile tbody,
.table-mobile tr,
.table-mobile td {
display: block;
width: 100%;
}
.table-mobile tr {
margin-bottom: var(--space-4);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
background: #fff;
box-shadow: var(--shadow-sm);
}
.table-mobile td {
padding: var(--space-3);
border: none;
border-bottom: 1px solid var(--color-gray-100);
position: relative;
padding-left: 40%;
}
.table-mobile td:last-child {
border-bottom: none;
}
.table-mobile td::before {
content: attr(data-label);
position: absolute;
left: var(--space-3);
top: 50%;
transform: translateY(-50%);
font-weight: 600;
color: var(--color-gray-600);
font-size: var(--text-xs);
width: 35%;
}
}
/* 防止水平滚动 */
body {
overflow-x: hidden;
}
/* 确保图片响应式 */
img {
max-width: 100%;
height: auto;
}
/* 响应式文本 */
@media (max-width: 768px) {
.panel-title {
font-size: var(--text-lg);
}
.muted {
font-size: var(--text-xs);
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,605 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const keywords = ref(['涉政', '广告推广', '色情', '辱骂', '造谣', '暴力', '诈骗', '侵权'])
const newKeyword = ref('')
const selectedCategory = ref('sensitive')
const strategySettings = ref({
aiReview: true,
highRiskBlock: true,
lowRiskDemote: true,
imageReview: true,
newUserStrict: true
})
const addKeyword = () => {
if (newKeyword.value.trim() && !keywords.value.includes(newKeyword.value.trim())) {
keywords.value.push(newKeyword.value.trim())
newKeyword.value = ''
}
}
const removeKeyword = (keyword) => {
keywords.value = keywords.value.filter(k => k !== keyword)
}
const categories = [
{ value: 'sensitive', label: '敏感词', icon: '🚫' },
{ value: 'ad', label: '广告推广', icon: '📢' },
{ value: 'abuse', label: '辱骂攻击', icon: '😠' },
{ value: 'fake', label: '虚假信息', icon: '❌' }
]
</script>
<template>
<LayoutFrame title="糖友圈智能审核设置">
<!-- 统计卡片 -->
<div class="cards-grid">
<div class="stat-card">
<div class="stat-value">{{ keywords.length }}</div>
<div class="muted">敏感词数量</div>
<div class="stat-trend positive">较上周 +3</div>
</div>
<div class="stat-card success">
<div class="stat-value">92.8%</div>
<div class="muted">AI 拦截命中率</div>
<div class="stat-trend positive">较上月 +2.5%</div>
</div>
<div class="stat-card">
<div class="stat-value">85.3%</div>
<div class="muted">人工复核通过率</div>
<div class="stat-trend positive">较上月 +1.8%</div>
</div>
<div class="stat-card warning">
<div class="stat-value">0.3%</div>
<div class="muted">误判率</div>
<div class="stat-trend negative">需持续优化</div>
</div>
</div>
<!-- 敏感词管理 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">敏感词管理</h2>
<div class="panel-actions">
<span class="badge">{{ keywords.length }} 个词条</span>
</div>
</div>
<div class="toolbar">
<select v-model="selectedCategory" class="select">
<option v-for="category in categories" :key="category.value" :value="category.value">
{{ category.icon }} {{ category.label }}
</option>
</select>
<input v-model="newKeyword" @keyup.enter="addKeyword" class="input" placeholder="输入新的敏感词..." />
<button @click="addKeyword" class="btn primary">添加词条</button>
<button class="btn secondary">批量导入</button>
<button class="btn secondary">导出词库</button>
</div>
<div class="keyword-section">
<div class="section-header">
<h3 class="section-title">当前敏感词库</h3>
<span class="section-subtitle"> {{ keywords.length }} 个敏感词</span>
</div>
<div class="keyword-list">
<div v-for="word in keywords" :key="word" class="keyword-tag">
<span class="keyword-text">{{ word }}</span>
<button @click="removeKeyword(word)" class="remove-btn" title="删除">
<span class="remove-icon">×</span>
</button>
</div>
<div v-if="keywords.length === 0" class="empty-state">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无敏感词请添加词条</div>
</div>
</div>
</div>
</div>
<!-- 审核策略 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">审核策略配置</h2>
<div class="strategy-status">
<span class="status-dot active"></span>
<span class="status-text">策略生效中</span>
</div>
</div>
<div class="strategy-content">
<div class="strategy-item" :class="{ active: strategySettings.aiReview }">
<div class="strategy-icon">🤖</div>
<div class="strategy-text">
<div class="strategy-title">启用 AI 智能审核</div>
<div class="strategy-desc">基于深度学习模型自动识别文本和图片违规内容</div>
</div>
<label class="switch">
<input v-model="strategySettings.aiReview" type="checkbox">
<span class="slider"></span>
</label>
</div>
<div class="strategy-item" :class="{ active: strategySettings.highRiskBlock }">
<div class="strategy-icon">🚫</div>
<div class="strategy-text">
<div class="strategy-title">高风险内容直接拦截</div>
<div class="strategy-desc">命中高风险词语的内容将直接拦截并进入人工复核队列</div>
</div>
<label class="switch">
<input v-model="strategySettings.highRiskBlock" type="checkbox">
<span class="slider"></span>
</label>
</div>
<div class="strategy-item" :class="{ active: strategySettings.lowRiskDemote }">
<div class="strategy-icon"></div>
<div class="strategy-text">
<div class="strategy-title">低风险内容降权展示</div>
<div class="strategy-desc">新用户发布的内容命中低风险词时自动降权限制传播范围</div>
</div>
<label class="switch">
<input v-model="strategySettings.lowRiskDemote" type="checkbox">
<span class="slider"></span>
</label>
</div>
<div class="strategy-item" :class="{ active: strategySettings.imageReview }">
<div class="strategy-icon">🖼</div>
<div class="strategy-text">
<div class="strategy-title">图片内容安全检测</div>
<div class="strategy-desc">启用图片OCR识别和色情暴力等视觉内容检测</div>
</div>
<label class="switch">
<input v-model="strategySettings.imageReview" type="checkbox">
<span class="slider"></span>
</label>
</div>
<div class="strategy-item" :class="{ active: strategySettings.newUserStrict }">
<div class="strategy-icon">👤</div>
<div class="strategy-text">
<div class="strategy-title">新用户严格审核</div>
<div class="strategy-desc">注册时间小于7天的用户发布内容需要更严格的审核</div>
</div>
<label class="switch">
<input v-model="strategySettings.newUserStrict" type="checkbox">
<span class="slider"></span>
</label>
</div>
</div>
<div class="strategy-footer">
<button class="btn secondary">恢复默认</button>
<button class="btn primary">保存策略</button>
</div>
</div>
<!-- 审核统计 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">审核数据统计</h2>
<div class="time-filter">
<select class="select small">
<option>今日</option>
<option>本周</option>
<option selected>本月</option>
<option>全年</option>
</select>
</div>
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">1,248</div>
<div class="stat-label">总审核量</div>
<div class="stat-change positive">+12%</div>
</div>
<div class="stat-item">
<div class="stat-number">89</div>
<div class="stat-label">拦截数量</div>
<div class="stat-change positive">+5%</div>
</div>
<div class="stat-item">
<div class="stat-number">7.1%</div>
<div class="stat-label">拦截率</div>
<div class="stat-change negative">-0.3%</div>
</div>
<div class="stat-item">
<div class="stat-number">23</div>
<div class="stat-label">人工复核</div>
<div class="stat-change positive">+8%</div>
</div>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.keyword-section {
margin: var(--space-5);
padding: var(--space-5);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-4);
}
.section-title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--color-gray-900);
margin: 0;
}
.section-subtitle {
font-size: var(--text-sm);
color: var(--color-gray-500);
font-weight: 500;
}
.keyword-list {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
min-height: 60px;
}
.keyword-tag {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: #fff;
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-xl);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-700);
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.keyword-tag::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background: var(--color-primary-500);
}
.keyword-tag:hover {
border-color: var(--color-primary-500);
background: var(--color-primary-50);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.keyword-text {
padding-right: var(--space-1);
}
.remove-btn {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border: none;
background: var(--color-gray-300);
color: var(--color-gray-600);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
line-height: 1;
}
.remove-btn:hover {
background: var(--color-error-500);
color: #fff;
transform: scale(1.1);
}
.remove-icon {
font-weight: bold;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
width: 100%;
padding: var(--space-8);
color: var(--color-gray-400);
}
.empty-icon {
font-size: 3rem;
margin-bottom: var(--space-2);
}
.empty-text {
font-size: var(--text-sm);
font-weight: 500;
}
.strategy-content {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: 0 var(--space-5);
}
.strategy-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-4) var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
transition: all 0.3s ease;
position: relative;
}
.strategy-item:hover {
background: var(--color-primary-50);
border-color: var(--color-primary-200);
transform: translateX(4px);
}
.strategy-item.active {
background: rgba(59, 130, 246, 0.05);
border-color: var(--color-primary-300);
}
.strategy-icon {
font-size: 1.5rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
.strategy-text {
flex: 1;
}
.strategy-title {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-1);
font-size: var(--text-base);
}
.strategy-desc {
font-size: var(--text-sm);
color: var(--color-gray-600);
line-height: var(--line-height-relaxed);
}
.switch {
position: relative;
display: inline-block;
width: 52px;
height: 28px;
flex-shrink: 0;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-gray-300);
transition: .4s;
border-radius: 28px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
input:checked + .slider {
background-color: var(--color-primary-500);
}
input:checked + .slider:before {
transform: translateX(24px);
}
.strategy-footer {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-5);
border-top: 1px solid var(--color-gray-200);
background: var(--color-gray-50);
}
.strategy-status {
display: flex;
align-items: center;
gap: var(--space-2);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-gray-400);
}
.status-dot.active {
background: var(--color-success-500);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.status-text {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-5);
padding: var(--space-5);
}
.stat-item {
text-align: center;
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
position: relative;
}
.stat-number {
font-size: 2rem;
font-weight: 800;
color: var(--color-gray-900);
line-height: 1;
margin-bottom: var(--space-2);
}
.stat-label {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
margin-bottom: var(--space-1);
}
.stat-change {
font-size: var(--text-xs);
font-weight: 600;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-xl);
display: inline-block;
}
.stat-change.positive {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-600);
}
.stat-change.negative {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-600);
}
.badge {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-3);
background: var(--color-primary-100);
color: var(--color-primary-700);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid var(--color-primary-200);
}
.time-filter {
display: flex;
align-items: center;
gap: var(--space-2);
}
.select.small {
min-width: 6rem;
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
}
/* 响应式设计 */
@media (max-width: 768px) {
.strategy-item {
flex-direction: column;
gap: var(--space-3);
text-align: center;
}
.strategy-text {
text-align: center;
}
.section-header {
flex-direction: column;
gap: var(--space-2);
align-items: flex-start;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
.strategy-footer {
flex-direction: column;
}
.strategy-footer .btn {
width: 100%;
}
}
@media (max-width: 640px) {
.stats-grid {
grid-template-columns: 1fr;
}
.keyword-tag {
flex: 1;
min-width: calc(50% - var(--space-3));
justify-content: space-between;
}
.toolbar {
flex-direction: column;
}
.toolbar .input,
.toolbar .select {
width: 100%;
}
}
</style>

@ -0,0 +1,909 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const users = ref([
{
id: 1,
name: '违规用户A',
avatar: '👤',
level: 'moderate',
reason: '频繁发布广告内容,涉及药品推广',
action: '封禁7天',
reports: 5,
lastActive: '2小时前',
joinDate: '2024-11-15',
violations: ['广告推广', '虚假宣传'],
status: 'banned'
},
{
id: 2,
name: '违规用户B',
avatar: '👤',
level: 'minor',
reason: '辱骂其他用户,语言攻击',
action: '封禁1天',
reports: 2,
lastActive: '1天前',
joinDate: '2024-12-10',
violations: ['辱骂攻击'],
status: 'banned'
},
{
id: 3,
name: '违规用户C',
avatar: '👤',
level: 'severe',
reason: '发布涉政敏感内容,多次警告无效',
action: '永久封禁',
reports: 12,
lastActive: '3天前',
joinDate: '2024-10-20',
violations: ['涉政内容', '多次违规'],
status: 'permanent_banned'
},
{
id: 4,
name: '风险用户D',
avatar: '👤',
level: 'warning',
reason: '发布疑似医疗建议,存在风险',
action: '内容限制',
reports: 3,
lastActive: '刚刚',
joinDate: '2024-12-18',
violations: ['风险内容'],
status: 'restricted'
}
])
const searchQuery = ref('')
const selectedLevel = ref('all')
const selectedStatus = ref('all')
const levelOptions = [
{ value: 'all', label: '全部等级', icon: '📊' },
{ value: 'severe', label: '严重违规', icon: '🔴' },
{ value: 'moderate', label: '中度违规', icon: '🟡' },
{ value: 'minor', label: '轻度违规', icon: '🟢' },
{ value: 'warning', label: '风险预警', icon: '🔵' }
]
const statusOptions = [
{ value: 'all', label: '全部状态', icon: '📋' },
{ value: 'banned', label: '封禁中', icon: '🚫' },
{ value: 'permanent_banned', label: '永久封禁', icon: '⛔' },
{ value: 'restricted', label: '限制中', icon: '⚠️' },
{ value: 'monitoring', label: '监控中', icon: '👁️' }
]
const getLevelInfo = (level) => {
const levelMap = {
severe: { label: '严重违规', color: 'red', icon: '🔴', score: 90 },
moderate: { label: '中度违规', color: 'orange', icon: '🟡', score: 70 },
minor: { label: '轻度违规', color: 'green', icon: '🟢', score: 50 },
warning: { label: '风险预警', color: 'blue', icon: '🔵', score: 30 }
}
return levelMap[level] || levelMap.warning
}
const getStatusInfo = (status) => {
const statusMap = {
banned: { label: '封禁中', color: 'orange', icon: '🚫' },
permanent_banned: { label: '永久封禁', color: 'red', icon: '⛔' },
restricted: { label: '限制中', color: 'blue', icon: '⚠️' },
monitoring: { label: '监控中', color: 'green', icon: '👁️' }
}
return statusMap[status] || statusMap.monitoring
}
const getActionInfo = (action) => {
if (action.includes('永久')) return { color: 'red', icon: '⛔' }
if (action.includes('封禁')) return { color: 'orange', icon: '🚫' }
return { color: 'blue', icon: '⚠️' }
}
const unbanUser = (userId) => {
const user = users.value.find(u => u.id === userId)
if (user && user.status !== 'permanent_banned') {
user.status = 'monitoring'
user.action = '已解封,持续监控'
}
}
const addToBlacklist = (userId) => {
const user = users.value.find(u => u.id === userId)
if (user) {
user.status = 'permanent_banned'
user.action = '已加入黑名单'
}
}
</script>
<template>
<LayoutFrame title="恶意用户管理">
<!-- 统计概览 -->
<div class="cards-grid">
<div class="stat-card warning">
<div class="stat-value">8</div>
<div class="muted">当前封禁用户</div>
<div class="stat-trend warning">较上周 +2</div>
</div>
<div class="stat-card danger">
<div class="stat-value">3</div>
<div class="muted">永久封禁用户</div>
<div class="stat-trend positive">本月新增 0</div>
</div>
<div class="stat-card">
<div class="stat-value">156</div>
<div class="muted">累计处理用户</div>
<div class="stat-trend positive">历史累计</div>
</div>
<div class="stat-card success">
<div class="stat-value">-24%</div>
<div class="muted">违规率下降</div>
<div class="stat-trend positive">治理成效显著</div>
</div>
</div>
<!-- 恶意用户管理 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">恶意用户列表</h2>
<div class="panel-actions">
<button class="btn primary">
<span class="btn-icon"></span>
添加黑名单
</button>
</div>
</div>
<!-- 搜索筛选 -->
<div class="toolbar">
<div class="filter-group">
<select v-model="selectedLevel" class="select">
<option v-for="option in levelOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
<select v-model="selectedStatus" class="select">
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
</div>
<div class="search-group">
<input v-model="searchQuery" class="input" placeholder="搜索用户、原因或处置措施..." />
<button class="btn primary">搜索</button>
</div>
<div class="action-group">
<button class="btn secondary">重置</button>
<button class="btn secondary">导出名单</button>
</div>
</div>
<!-- 用户列表 -->
<div class="table-container">
<table class="table">
<thead>
<tr>
<th style="width: 120px;">用户信息</th>
<th style="width: 100px;">风险等级</th>
<th style="width: 200px;">违规原因</th>
<th style="width: 80px;">举报次数</th>
<th style="width: 120px;">违规记录</th>
<th style="width: 100px;">处置措施</th>
<th style="width: 120px;">用户状态</th>
<th style="width: 150px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in users" :key="item.id" class="user-row">
<td>
<div class="user-info">
<div class="user-avatar">{{ item.avatar }}</div>
<div class="user-details">
<div class="user-name">{{ item.name }}</div>
<div class="user-meta">
<span class="join-date">注册: {{ item.joinDate }}</span>
</div>
</div>
</div>
</td>
<td>
<div class="risk-level">
<div class="level-indicator" :class="getLevelInfo(item.level).color">
<span class="level-icon">{{ getLevelInfo(item.level).icon }}</span>
<span class="level-label">{{ getLevelInfo(item.level).label }}</span>
</div>
<div class="risk-score">
<div class="score-bar">
<div class="score-fill" :style="{ width: getLevelInfo(item.level).score + '%' }"></div>
</div>
<span class="score-value">{{ getLevelInfo(item.level).score }}</span>
</div>
</div>
</td>
<td>
<div class="reason-content">
<p class="reason-text">{{ item.reason }}</p>
<div class="last-active">最后活跃: {{ item.lastActive }}</div>
</div>
</td>
<td>
<div class="reports-count">
<span class="count-badge">{{ item.reports }}</span>
<span class="count-label">次举报</span>
</div>
</td>
<td>
<div class="violations-list">
<span v-for="violation in item.violations" :key="violation" class="violation-tag">
{{ violation }}
</span>
</div>
</td>
<td>
<div class="action-info" :class="getActionInfo(item.action).color">
<span class="action-icon">{{ getActionInfo(item.action).icon }}</span>
<span class="action-text">{{ item.action }}</span>
</div>
</td>
<td>
<span class="status-tag" :class="getStatusInfo(item.status).color">
<span class="status-icon">{{ getStatusInfo(item.status).icon }}</span>
{{ getStatusInfo(item.status).label }}
</span>
</td>
<td>
<div class="action-buttons">
<button v-if="item.status !== 'permanent_banned'" @click="unbanUser(item.id)" class="btn success small">
<span class="btn-icon">🔓</span>
解封
</button>
<button v-if="item.status !== 'permanent_banned'" @click="addToBlacklist(item.id)" class="btn danger small">
<span class="btn-icon"></span>
永久封禁
</button>
<button class="btn secondary small">
<span class="btn-icon">📋</span>
详情
</button>
<button class="btn secondary small">
<span class="btn-icon"></span>
通知
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="table-footer">
<div class="pagination-info">
显示 1-4 4 条记录
</div>
<div class="pagination">
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button></button>
</div>
</div>
</div>
<!-- 风险分析 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">风险分析</h3>
<div class="time-filter">
<select class="select small">
<option>今日</option>
<option>本周</option>
<option selected>本月</option>
<option>全年</option>
</select>
</div>
</div>
<div class="risk-analysis">
<div class="analysis-chart">
<div class="chart-title">违规类型分布</div>
<div class="chart-grid">
<div class="chart-item">
<div class="chart-type">广告推广</div>
<div class="chart-bar">
<div class="bar-fill" style="width: 65%; background: var(--color-error-500);"></div>
</div>
<div class="chart-value">65%</div>
</div>
<div class="chart-item">
<div class="chart-type">辱骂攻击</div>
<div class="chart-bar">
<div class="bar-fill" style="width: 20%; background: var(--color-warning-500);"></div>
</div>
<div class="chart-value">20%</div>
</div>
<div class="chart-item">
<div class="chart-type">虚假信息</div>
<div class="chart-bar">
<div class="bar-fill" style="width: 10%; background: var(--color-primary-500);"></div>
</div>
<div class="chart-value">10%</div>
</div>
<div class="chart-item">
<div class="chart-type">其他违规</div>
<div class="chart-bar">
<div class="bar-fill" style="width: 5%; background: var(--color-gray-500);"></div>
</div>
<div class="chart-value">5%</div>
</div>
</div>
</div>
<div class="analysis-stats">
<div class="stat-card small">
<div class="stat-value">12</div>
<div class="stat-label">新增风险用户</div>
<div class="stat-trend negative">较上月 +3</div>
</div>
<div class="stat-card small">
<div class="stat-value">2.1</div>
<div class="stat-label">平均违规次数</div>
<div class="stat-trend positive">较上月 -0.3</div>
</div>
<div class="stat-card small">
<div class="stat-value">78%</div>
<div class="stat-label">处置有效率</div>
<div class="stat-trend positive">表现良好</div>
</div>
</div>
</div>
</div>
<!-- 批量操作 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">批量操作</h3>
</div>
<div class="batch-actions">
<div class="action-group">
<label class="action-label">选择操作:</label>
<select class="select">
<option>批量解封</option>
<option>批量封禁</option>
<option>批量限制</option>
<option>批量导出</option>
</select>
<button class="btn primary">执行操作</button>
</div>
<div class="action-group">
<label class="action-label">自动处理规则:</label>
<div class="rule-settings">
<label class="checkbox">
<input type="checkbox" checked />
<span class="checkmark"></span>
自动封禁多次违规用户
</label>
<label class="checkbox">
<input type="checkbox" checked />
<span class="checkmark"></span>
自动限制风险内容发布
</label>
<label class="checkbox">
<input type="checkbox" />
<span class="checkmark"></span>
自动发送警告通知
</label>
</div>
</div>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.user-row {
transition: all 0.3s ease;
}
.user-row:hover {
background: var(--color-primary-50) !important;
}
.user-info {
display: flex;
align-items: center;
gap: var(--space-3);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--color-gray-200);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
flex-shrink: 0;
}
.user-details {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-1);
font-size: var(--text-sm);
}
.user-meta {
font-size: var(--text-xs);
color: var(--color-gray-500);
}
.join-date {
display: block;
}
.risk-level {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.level-indicator {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.level-indicator.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.level-indicator.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.level-indicator.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.level-indicator.blue {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
border-color: rgba(59, 130, 246, 0.2);
}
.level-icon {
font-size: var(--text-xs);
}
.risk-score {
display: flex;
align-items: center;
gap: var(--space-2);
}
.score-bar {
flex: 1;
height: 4px;
background: var(--color-gray-200);
border-radius: var(--radius-xl);
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: var(--radius-xl);
transition: width 0.3s ease;
background: var(--color-primary-500);
}
.score-value {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-gray-600);
min-width: 30px;
}
.reason-content {
max-width: 180px;
}
.reason-text {
font-size: var(--text-sm);
color: var(--color-gray-700);
margin: 0 0 var(--space-1);
line-height: var(--line-height-relaxed);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.last-active {
font-size: var(--text-xs);
color: var(--color-gray-500);
}
.reports-count {
display: flex;
align-items: center;
gap: var(--space-1);
}
.count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: var(--color-error-500);
color: #fff;
border-radius: 50%;
font-size: var(--text-xs);
font-weight: 700;
}
.count-label {
font-size: var(--text-xs);
color: var(--color-gray-600);
font-weight: 500;
}
.violations-list {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.violation-tag {
padding: var(--space-1) var(--space-2);
background: var(--color-gray-100);
color: var(--color-gray-600);
border-radius: var(--radius);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--color-gray-200);
text-align: center;
}
.action-info {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.action-info.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.action-info.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.action-info.blue {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
border-color: rgba(59, 130, 246, 0.2);
}
.action-icon {
font-size: var(--text-xs);
}
.status-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.status-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.status-tag.blue {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
border-color: rgba(59, 130, 246, 0.2);
}
.status-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.status-icon {
font-size: var(--text-xs);
}
.action-buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.btn.success {
background: linear-gradient(135deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.success:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn.danger {
background: linear-gradient(135deg, var(--color-error-500) 0%, var(--color-error-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.danger:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.risk-analysis {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-6);
padding: var(--space-5);
}
.analysis-chart {
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
}
.chart-title {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-4);
font-size: var(--text-lg);
}
.chart-grid {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.chart-item {
display: flex;
align-items: center;
gap: var(--space-3);
}
.chart-type {
width: 80px;
font-size: var(--text-sm);
color: var(--color-gray-700);
font-weight: 500;
}
.chart-bar {
flex: 1;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-xl);
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: var(--radius-xl);
transition: width 0.3s ease;
}
.chart-value {
width: 40px;
text-align: right;
font-weight: 600;
color: var(--color-gray-700);
font-size: var(--text-sm);
}
.analysis-stats {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.stat-card.small {
padding: var(--space-3);
text-align: center;
}
.stat-card.small .stat-value {
font-size: 1.5rem;
}
.batch-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-6);
padding: var(--space-5);
}
.action-group {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.action-label {
font-weight: 600;
color: var(--color-gray-700);
font-size: var(--text-sm);
}
.rule-settings {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
font-size: var(--text-sm);
color: var(--color-gray-700);
}
.checkbox input {
display: none;
}
.checkmark {
width: 16px;
height: 16px;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-sm);
position: relative;
transition: all 0.2s ease;
}
.checkbox input:checked + .checkmark {
background: var(--color-primary-500);
border-color: var(--color-primary-500);
}
.checkbox input:checked + .checkmark::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 10px;
font-weight: bold;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
gap: var(--space-4);
}
.filter-group,
.search-group,
.action-group {
width: 100%;
}
.table-container {
overflow-x: auto;
}
.risk-analysis {
grid-template-columns: 1fr;
gap: var(--space-4);
}
.batch-actions {
grid-template-columns: 1fr;
gap: var(--space-4);
}
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
.analysis-stats {
grid-template-columns: repeat(2, 1fr);
}
.user-info {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr 1fr;
}
.analysis-stats {
grid-template-columns: 1fr;
}
.chart-item {
flex-direction: column;
gap: var(--space-2);
align-items: stretch;
}
.chart-type {
width: auto;
}
.chart-value {
width: auto;
text-align: left;
}
}
</style>

@ -0,0 +1,616 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const searchQuery = ref('')
const selectedStatus = ref('all')
const selectedType = ref('all')
const posts = ref([
{
id: 1,
author: '糖友小白',
title: '今天跑步 5km血糖控制得不错',
status: 'pending',
time: '10:32',
type: 'dynamic',
excerpt: '坚持运动真的很重要今天晨跑5公里餐后血糖6.8,继续保持!',
likes: 24,
comments: 8,
views: 156
},
{
id: 2,
author: '控糖达人',
title: '分享我的糖尿病饮食计划表',
status: 'published',
time: '09:20',
type: 'article',
excerpt: '经过多年摸索总结的饮食计划,希望对大家有帮助...',
likes: 89,
comments: 23,
views: 1245
},
{
id: 3,
author: '刘老师',
title: '问:餐后血糖偏高怎么办?',
status: 'published',
time: '08:55',
type: 'question',
excerpt: '最近餐后血糖总是在9-10之间饮食已经很控制了求建议...',
likes: 15,
comments: 34,
views: 289
},
{
id: 4,
author: '健康生活',
title: '糖尿病运动注意事项大全',
status: 'rejected',
time: '昨天 14:20',
type: 'article',
excerpt: '详细整理了糖尿病患者的运动注意事项和推荐方案...',
likes: 0,
comments: 0,
views: 0
}
])
const statusOptions = [
{ value: 'all', label: '全部状态', icon: '📋' },
{ value: 'pending', label: '待审核', icon: '⏳' },
{ value: 'published', label: '已发布', icon: '✅' },
{ value: 'rejected', label: '已拒绝', icon: '❌' }
]
const typeOptions = [
{ value: 'all', label: '全部类型', icon: '📄' },
{ value: 'dynamic', label: '动态', icon: '💬' },
{ value: 'article', label: '文章', icon: '📝' },
{ value: 'question', label: '问答', icon: '❓' }
]
const getStatusInfo = (status) => {
const statusMap = {
pending: { label: '待审核', color: 'orange', icon: '⏳' },
published: { label: '已发布', color: 'green', icon: '✅' },
rejected: { label: '已拒绝', color: 'red', icon: '❌' }
}
return statusMap[status] || statusMap.pending
}
const getTypeInfo = (type) => {
const typeMap = {
dynamic: { label: '动态', color: 'blue', icon: '💬' },
article: { label: '文章', color: 'green', icon: '📝' },
question: { label: '问答', color: 'orange', icon: '❓' }
}
return typeMap[type] || typeMap.dynamic
}
const approvePost = (postId) => {
const post = posts.value.find(p => p.id === postId)
if (post) {
post.status = 'published'
}
}
const rejectPost = (postId) => {
const post = posts.value.find(p => p.id === postId)
if (post) {
post.status = 'rejected'
}
}
</script>
<template>
<LayoutFrame title="糖友圈发布管理">
<!-- 统计概览 -->
<div class="cards-grid">
<div class="stat-card">
<div class="stat-value">62</div>
<div class="muted">今日发布</div>
<div class="stat-trend positive">较昨日 +8%</div>
</div>
<div class="stat-card warning">
<div class="stat-value">5</div>
<div class="muted">待审核</div>
<div class="stat-trend warning">需及时处理</div>
</div>
<div class="stat-card danger">
<div class="stat-value">2</div>
<div class="muted">违规拦截</div>
<div class="stat-trend negative">较昨日 -1</div>
</div>
<div class="stat-card success">
<div class="stat-value">98.3%</div>
<div class="muted">审核通过率</div>
<div class="stat-trend positive">质量良好</div>
</div>
</div>
<!-- 内容管理 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">内容管理</h2>
<div class="panel-actions">
<button class="btn primary">
<span class="btn-icon"></span>
新建发布
</button>
</div>
</div>
<!-- 搜索筛选 -->
<div class="toolbar">
<div class="filter-group">
<select v-model="selectedStatus" class="select">
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
<select v-model="selectedType" class="select">
<option v-for="option in typeOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
</div>
<div class="search-group">
<input v-model="searchQuery" class="input" placeholder="搜索标题、作者或内容..." />
<button class="btn primary">搜索</button>
</div>
<div class="action-group">
<button class="btn secondary">重置</button>
<button class="btn secondary">导出数据</button>
</div>
</div>
<!-- 内容列表 -->
<div class="table-container">
<table class="table">
<thead>
<tr>
<th style="width: 300px;">内容</th>
<th style="width: 100px;">作者</th>
<th style="width: 80px;">类型</th>
<th style="width: 100px;">互动数据</th>
<th style="width: 100px;">状态</th>
<th style="width: 120px;">发布时间</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in posts" :key="item.id" class="post-row">
<td>
<div class="post-content">
<h4 class="post-title">{{ item.title }}</h4>
<p class="post-excerpt">{{ item.excerpt }}</p>
</div>
</td>
<td>
<div class="author-info">
<span class="author-avatar">👤</span>
<span class="author-name">{{ item.author }}</span>
</div>
</td>
<td>
<span class="type-tag" :class="getTypeInfo(item.type).color">
<span class="type-icon">{{ getTypeInfo(item.type).icon }}</span>
{{ getTypeInfo(item.type).label }}
</span>
</td>
<td>
<div class="interaction-stats">
<div class="stat">
<span class="stat-icon"></span>
<span class="stat-value">{{ item.likes }}</span>
</div>
<div class="stat">
<span class="stat-icon">💬</span>
<span class="stat-value">{{ item.comments }}</span>
</div>
<div class="stat">
<span class="stat-icon">👁</span>
<span class="stat-value">{{ item.views }}</span>
</div>
</div>
</td>
<td>
<span class="status-tag" :class="getStatusInfo(item.status).color">
<span class="status-icon">{{ getStatusInfo(item.status).icon }}</span>
{{ getStatusInfo(item.status).label }}
</span>
</td>
<td>
<div class="time-info">
<div class="time">{{ item.time }}</div>
<div class="date muted">2024-12-24</div>
</div>
</td>
<td>
<div class="action-buttons">
<button v-if="item.status === 'pending'" @click="approvePost(item.id)" class="btn success small">
<span class="btn-icon"></span>
通过
</button>
<button v-if="item.status === 'pending'" @click="rejectPost(item.id)" class="btn danger small">
<span class="btn-icon"></span>
拒绝
</button>
<button class="btn secondary small">
<span class="btn-icon">👁</span>
预览
</button>
<button class="btn secondary small">
<span class="btn-icon"></span>
编辑
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="table-footer">
<div class="pagination-info">
显示 1-4 4 条记录
</div>
<div class="pagination">
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button></button>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">快速操作</h3>
</div>
<div class="quick-actions">
<button class="quick-action-btn">
<span class="action-icon">📊</span>
<span class="action-text">数据统计</span>
</button>
<button class="quick-action-btn">
<span class="action-icon"></span>
<span class="action-text">审核设置</span>
</button>
<button class="quick-action-btn">
<span class="action-icon">👥</span>
<span class="action-text">用户管理</span>
</button>
<button class="quick-action-btn">
<span class="action-icon">📋</span>
<span class="action-text">内容模板</span>
</button>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.filter-group {
display: flex;
gap: var(--space-3);
}
.search-group {
display: flex;
gap: var(--space-3);
flex: 1;
}
.search-group .input {
flex: 1;
}
.action-group {
display: flex;
gap: var(--space-3);
}
.post-row {
transition: all 0.3s ease;
}
.post-row:hover {
background: var(--color-primary-50) !important;
}
.post-content {
max-width: 280px;
}
.post-title {
font-weight: 600;
color: var(--color-gray-900);
margin: 0 0 var(--space-2);
font-size: var(--text-sm);
line-height: var(--line-height-tight);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-excerpt {
font-size: var(--text-xs);
color: var(--color-gray-600);
margin: 0;
line-height: var(--line-height-relaxed);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.author-info {
display: flex;
align-items: center;
gap: var(--space-2);
}
.author-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--color-gray-200);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
}
.author-name {
font-weight: 500;
color: var(--color-gray-700);
}
.type-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid;
}
.type-tag.blue {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
border-color: rgba(59, 130, 246, 0.2);
}
.type-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.type-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.type-icon {
font-size: var(--text-xs);
}
.interaction-stats {
display: flex;
gap: var(--space-3);
}
.stat {
display: flex;
align-items: center;
gap: var(--space-1);
}
.stat-icon {
font-size: var(--text-xs);
opacity: 0.7;
}
.stat-value {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-gray-700);
}
.status-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.status-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.status-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.status-icon {
font-size: var(--text-xs);
}
.time-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.time {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.date {
font-size: var(--text-xs);
}
.action-buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.btn.success {
background: linear-gradient(135deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.success:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn.danger {
background: linear-gradient(135deg, var(--color-error-500) 0%, var(--color-error-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.danger:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn-icon {
font-size: var(--text-sm);
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: var(--space-4);
padding: var(--space-5);
}
.quick-action-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-4);
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.quick-action-btn:hover {
background: var(--color-primary-50);
border-color: var(--color-primary-300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.action-icon {
font-size: 2rem;
margin-bottom: var(--space-1);
}
.action-text {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-gray-700);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
gap: var(--space-4);
}
.filter-group,
.search-group,
.action-group {
width: 100%;
}
.table-container {
overflow-x: auto;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
}
@media (max-width: 768px) {
.post-content {
max-width: 200px;
}
.interaction-stats {
flex-direction: column;
gap: var(--space-1);
}
.quick-actions {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr 1fr;
}
.quick-actions {
grid-template-columns: 1fr;
}
.table-footer {
flex-direction: column;
gap: var(--space-3);
}
.pagination {
flex-wrap: wrap;
justify-content: center;
}
}
</style>

@ -0,0 +1,819 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const reports = ref([
{
id: 1,
reason: '广告推广',
post: '减肥药推广链接,声称快速降糖无副作用...',
user: '陌生用户',
reporter: '糖友小白',
status: 'processing',
time: '今天 11:20',
priority: 'high',
type: 'post',
evidence: ['截图1', '截图2']
},
{
id: 2,
reason: '辱骂他人',
post: '评论:你根本不懂糖尿病,在这里误导别人!',
user: '糖友小白',
reporter: '控糖达人',
status: 'resolved',
time: '今天 09:15',
priority: 'medium',
type: 'comment',
action: '删除评论,警告用户'
},
{
id: 3,
reason: '虚假信息',
post: '血糖仪虚假参数,夸大测量精度和功能...',
user: '张三',
reporter: '李四',
status: 'processing',
time: '昨天 20:33',
priority: 'high',
type: 'post',
evidence: ['产品截图', '聊天记录']
},
{
id: 4,
reason: '色情内容',
post: '不当图片和暗示性文字内容...',
user: '违规用户A',
reporter: '匿名用户',
status: 'resolved',
time: '昨天 16:45',
priority: 'high',
type: 'post',
action: '删除内容封禁用户7天'
}
])
const searchQuery = ref('')
const selectedStatus = ref('all')
const selectedPriority = ref('all')
const statusOptions = [
{ value: 'all', label: '全部状态', icon: '📋' },
{ value: 'processing', label: '处理中', icon: '⏳' },
{ value: 'resolved', label: '已处理', icon: '✅' },
{ value: 'rejected', label: '已驳回', icon: '❌' }
]
const priorityOptions = [
{ value: 'all', label: '全部优先级', icon: '📊' },
{ value: 'high', label: '高优先级', icon: '🔴' },
{ value: 'medium', label: '中优先级', icon: '🟡' },
{ value: 'low', label: '低优先级', icon: '🟢' }
]
const getStatusInfo = (status) => {
const statusMap = {
processing: { label: '处理中', color: 'orange', icon: '⏳' },
resolved: { label: '已处理', color: 'green', icon: '✅' },
rejected: { label: '已驳回', color: 'red', icon: '❌' }
}
return statusMap[status] || statusMap.processing
}
const getPriorityInfo = (priority) => {
const priorityMap = {
high: { label: '高', color: 'red', icon: '🔴' },
medium: { label: '中', color: 'orange', icon: '🟡' },
low: { label: '低', color: 'green', icon: '🟢' }
}
return priorityMap[priority] || priorityMap.medium
}
const getTypeInfo = (type) => {
const typeMap = {
post: { label: '帖子', color: 'blue', icon: '📝' },
comment: { label: '评论', color: 'green', icon: '💬' },
reply: { label: '回复', color: 'orange', icon: '↩️' }
}
return typeMap[type] || typeMap.post
}
const resolveReport = (reportId) => {
const report = reports.value.find(r => r.id === reportId)
if (report) {
report.status = 'resolved'
report.action = '已处理,内容已审核'
}
}
const rejectReport = (reportId) => {
const report = reports.value.find(r => r.id === reportId)
if (report) {
report.status = 'rejected'
report.action = '举报不成立,已驳回'
}
}
</script>
<template>
<LayoutFrame title="糖友圈举报管理">
<!-- 统计概览 -->
<div class="cards-grid">
<div class="stat-card warning">
<div class="stat-value">14</div>
<div class="muted">今日举报</div>
<div class="stat-trend positive">较昨日 -2</div>
</div>
<div class="stat-card">
<div class="stat-value">7</div>
<div class="muted">处理中</div>
<div class="stat-trend warning">需及时处理</div>
</div>
<div class="stat-card success">
<div class="stat-value">120</div>
<div class="muted">已处置</div>
<div class="stat-trend positive">本月累计</div>
</div>
<div class="stat-card">
<div class="stat-value">92.5%</div>
<div class="muted">处理效率</div>
<div class="stat-trend positive">表现优秀</div>
</div>
</div>
<!-- 举报管理 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">举报列表</h2>
<div class="panel-actions">
<button class="btn primary">
<span class="btn-icon">📊</span>
数据报表
</button>
</div>
</div>
<!-- 搜索筛选 -->
<div class="toolbar">
<div class="filter-group">
<select v-model="selectedStatus" class="select">
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
<select v-model="selectedPriority" class="select">
<option v-for="option in priorityOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
</div>
<div class="search-group">
<input v-model="searchQuery" class="input" placeholder="搜索举报原因、内容或用户..." />
<button class="btn primary">搜索</button>
</div>
<div class="action-group">
<button class="btn secondary">重置</button>
<button class="btn secondary">导出结果</button>
</div>
</div>
<!-- 举报列表 -->
<div class="table-container">
<table class="table">
<thead>
<tr>
<th style="width: 120px;">举报信息</th>
<th style="width: 200px;">内容摘要</th>
<th style="width: 100px;">被举报人</th>
<th style="width: 100px;">举报人</th>
<th style="width: 80px;">内容类型</th>
<th style="width: 80px;">优先级</th>
<th style="width: 100px;">状态</th>
<th style="width: 120px;">举报时间</th>
<th style="width: 200px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in reports" :key="item.id" class="report-row">
<td>
<div class="report-info">
<div class="report-reason">
<span class="reason-icon">🚩</span>
{{ item.reason }}
</div>
<div class="report-id">#{{ item.id }}</div>
</div>
</td>
<td>
<div class="post-content">
<p class="post-excerpt">{{ item.post }}</p>
<div v-if="item.evidence" class="evidence-tags">
<span v-for="(evidence, index) in item.evidence" :key="index" class="evidence-tag">
{{ evidence }}
</span>
</div>
</div>
</td>
<td>
<div class="user-info reported">
<span class="user-avatar">👤</span>
<span class="user-name">{{ item.user }}</span>
</div>
</td>
<td>
<div class="user-info reporter">
<span class="user-avatar">👤</span>
<span class="user-name">{{ item.reporter }}</span>
</div>
</td>
<td>
<span class="type-tag" :class="getTypeInfo(item.type).color">
<span class="type-icon">{{ getTypeInfo(item.type).icon }}</span>
{{ getTypeInfo(item.type).label }}
</span>
</td>
<td>
<span class="priority-tag" :class="getPriorityInfo(item.priority).color">
<span class="priority-icon">{{ getPriorityInfo(item.priority).icon }}</span>
{{ getPriorityInfo(item.priority).label }}
</span>
</td>
<td>
<span class="status-tag" :class="getStatusInfo(item.status).color">
<span class="status-icon">{{ getStatusInfo(item.status).icon }}</span>
{{ getStatusInfo(item.status).label }}
</span>
</td>
<td>
<div class="time-info">
<div class="time">{{ item.time }}</div>
<div v-if="item.action" class="action-taken muted">
{{ item.action }}
</div>
</div>
</td>
<td>
<div class="action-buttons">
<button v-if="item.status === 'processing'" @click="resolveReport(item.id)" class="btn success small">
<span class="btn-icon"></span>
处理完成
</button>
<button v-if="item.status === 'processing'" @click="rejectReport(item.id)" class="btn danger small">
<span class="btn-icon"></span>
驳回举报
</button>
<button class="btn secondary small">
<span class="btn-icon">👁</span>
查看详情
</button>
<button class="btn secondary small">
<span class="btn-icon">📞</span>
联系用户
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="table-footer">
<div class="pagination-info">
显示 1-4 4 条记录
</div>
<div class="pagination">
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button></button>
</div>
</div>
</div>
<!-- 举报分析 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">举报分析</h3>
<div class="time-filter">
<select class="select small">
<option>今日</option>
<option>本周</option>
<option selected>本月</option>
<option>全年</option>
</select>
</div>
</div>
<div class="analysis-grid">
<div class="analysis-chart">
<div class="chart-title">举报类型分布</div>
<div class="chart-content">
<div class="chart-bar">
<div class="bar-label">广告推广</div>
<div class="bar-container">
<div class="bar-fill" style="width: 45%; background: var(--color-error-500);"></div>
</div>
<div class="bar-value">45%</div>
</div>
<div class="chart-bar">
<div class="bar-label">辱骂攻击</div>
<div class="bar-container">
<div class="bar-fill" style="width: 25%; background: var(--color-warning-500);"></div>
</div>
<div class="bar-value">25%</div>
</div>
<div class="chart-bar">
<div class="bar-label">虚假信息</div>
<div class="bar-container">
<div class="bar-fill" style="width: 15%; background: var(--color-primary-500);"></div>
</div>
<div class="bar-value">15%</div>
</div>
<div class="chart-bar">
<div class="bar-label">色情内容</div>
<div class="bar-container">
<div class="bar-fill" style="width: 10%; background: var(--color-success-500);"></div>
</div>
<div class="bar-value">10%</div>
</div>
<div class="chart-bar">
<div class="bar-label">其他</div>
<div class="bar-container">
<div class="bar-fill" style="width: 5%; background: var(--color-gray-500);"></div>
</div>
<div class="bar-value">5%</div>
</div>
</div>
</div>
<div class="analysis-stats">
<div class="stat-item">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-value">2.3小时</div>
<div class="stat-label">平均处理时间</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-value">88.7%</div>
<div class="stat-label">有效举报率</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">👥</div>
<div class="stat-content">
<div class="stat-value">23</div>
<div class="stat-label">重复举报用户</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">📈</div>
<div class="stat-content">
<div class="stat-value">-15%</div>
<div class="stat-label">举报量趋势</div>
</div>
</div>
</div>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.report-row {
transition: all 0.3s ease;
}
.report-row:hover {
background: var(--color-primary-50) !important;
}
.report-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.report-reason {
display: flex;
align-items: center;
gap: var(--space-1);
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.reason-icon {
font-size: var(--text-xs);
}
.report-id {
font-size: var(--text-xs);
color: var(--color-gray-500);
font-weight: 500;
}
.post-content {
max-width: 180px;
}
.post-excerpt {
font-size: var(--text-sm);
color: var(--color-gray-700);
margin: 0 0 var(--space-2);
line-height: var(--line-height-relaxed);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.evidence-tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
.evidence-tag {
padding: var(--space-1) var(--space-2);
background: var(--color-gray-100);
color: var(--color-gray-600);
border-radius: var(--radius);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--color-gray-200);
}
.user-info {
display: flex;
align-items: center;
gap: var(--space-2);
}
.user-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--color-gray-200);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
}
.user-name {
font-weight: 500;
color: var(--color-gray-700);
font-size: var(--text-sm);
}
.user-info.reported .user-avatar {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.user-info.reporter .user-avatar {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.type-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid;
}
.type-tag.blue {
background: rgba(59, 130, 246, 0.1);
color: var(--color-primary-700);
border-color: rgba(59, 130, 246, 0.2);
}
.type-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.type-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.type-icon {
font-size: var(--text-xs);
}
.priority-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.priority-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.priority-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.priority-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.priority-icon {
font-size: var(--text-xs);
}
.status-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.status-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.status-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.status-icon {
font-size: var(--text-xs);
}
.time-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.time {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.action-taken {
font-size: var(--text-xs);
max-width: 100px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.action-buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.btn.success {
background: linear-gradient(135deg, var(--color-success-500) 0%, var(--color-success-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.success:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn.danger {
background: linear-gradient(135deg, var(--color-error-500) 0%, var(--color-error-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.danger:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.analysis-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-6);
padding: var(--space-5);
}
.analysis-chart {
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
}
.chart-title {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-4);
font-size: var(--text-lg);
}
.chart-content {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.chart-bar {
display: flex;
align-items: center;
gap: var(--space-3);
}
.bar-label {
width: 80px;
font-size: var(--text-sm);
color: var(--color-gray-700);
font-weight: 500;
}
.bar-container {
flex: 1;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-xl);
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: var(--radius-xl);
transition: width 0.3s ease;
}
.bar-value {
width: 40px;
text-align: right;
font-weight: 600;
color: var(--color-gray-700);
font-size: var(--text-sm);
}
.analysis-stats {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
transition: all 0.3s ease;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary-300);
}
.stat-icon {
font-size: 1.5rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--text-xl);
font-weight: 800;
color: var(--color-gray-900);
line-height: 1;
margin-bottom: var(--space-1);
}
.stat-label {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
gap: var(--space-4);
}
.filter-group,
.search-group,
.action-group {
width: 100%;
}
.table-container {
overflow-x: auto;
}
.analysis-grid {
grid-template-columns: 1fr;
gap: var(--space-4);
}
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
.analysis-stats {
grid-template-columns: repeat(2, 1fr);
}
.stat-item {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr 1fr;
}
.analysis-stats {
grid-template-columns: 1fr;
}
.chart-bar {
flex-direction: column;
gap: var(--space-2);
align-items: stretch;
}
.bar-label {
width: auto;
}
.bar-value {
width: auto;
text-align: left;
}
}
</style>

@ -0,0 +1,851 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const customers = ref([
{
id: 1,
name: '李雷',
avatar: '👨',
phone: '13800001111',
level: 'high',
tags: ['糖尿病', '重点跟进', '设备用户'],
date: '2025-01-05',
status: 'active',
lastContact: '今天 09:30',
orders: 12,
totalSpent: 2840,
healthScore: 85
},
{
id: 2,
name: '韩梅梅',
avatar: '👩',
phone: '13900002222',
level: 'medium',
tags: ['设备用户', '活跃'],
date: '2025-01-03',
status: 'active',
lastContact: '昨天 14:20',
orders: 8,
totalSpent: 1560,
healthScore: 72
},
{
id: 3,
name: '王强',
avatar: '👨',
phone: '13700003333',
level: 'low',
tags: ['新注册', '咨询中'],
date: '2025-01-02',
status: 'inactive',
lastContact: '3天前',
orders: 2,
totalSpent: 320,
healthScore: 45
},
{
id: 4,
name: '赵小云',
avatar: '👩',
phone: '13600004444',
level: 'high',
tags: ['糖尿病', 'VIP客户', '长期随访'],
date: '2024-12-28',
status: 'active',
lastContact: '今天 11:15',
orders: 25,
totalSpent: 5890,
healthScore: 92
}
])
const searchQuery = ref('')
const selectedLevel = ref('all')
const selectedStatus = ref('all')
const levelOptions = [
{ value: 'all', label: '全部优先级', icon: '📊' },
{ value: 'high', label: '高优先级', icon: '🔴' },
{ value: 'medium', label: '中优先级', icon: '🟡' },
{ value: 'low', label: '低优先级', icon: '🟢' }
]
const statusOptions = [
{ value: 'all', label: '全部状态', icon: '📋' },
{ value: 'active', label: '活跃', icon: '✅' },
{ value: 'inactive', label: '沉默', icon: '⏸️' },
{ value: 'lost', label: '流失', icon: '❌' }
]
const getLevelInfo = (level) => {
const levelMap = {
high: { label: '高', color: 'red', icon: '🔴' },
medium: { label: '中', color: 'orange', icon: '🟡' },
low: { label: '低', color: 'green', icon: '🟢' }
}
return levelMap[level] || levelMap.medium
}
const getStatusInfo = (status) => {
const statusMap = {
active: { label: '活跃', color: 'green', icon: '✅' },
inactive: { label: '沉默', color: 'orange', icon: '⏸️' },
lost: { label: '流失', color: 'red', icon: '❌' }
}
return statusMap[status] || statusMap.active
}
const getHealthScoreInfo = (score) => {
if (score >= 80) return { color: 'green', label: '优秀' }
if (score >= 60) return { color: 'orange', label: '良好' }
return { color: 'red', label: '需关注' }
}
const contactCustomer = (customerId) => {
//
console.log('联系客户:', customerId)
}
const viewDetails = (customerId) => {
//
console.log('查看客户详情:', customerId)
}
</script>
<template>
<LayoutFrame title="客户管理">
<!-- 统计概览 -->
<div class="cards-grid">
<div class="stat-card">
<div class="stat-value">245</div>
<div class="muted">本月新增</div>
<div class="stat-trend positive">对比上月 +8%</div>
</div>
<div class="stat-card warning">
<div class="stat-value">36</div>
<div class="muted">高优先</div>
<div class="stat-desc">持续跟进</div>
</div>
<div class="stat-card success">
<div class="stat-value">812</div>
<div class="muted">设备用户</div>
<div class="stat-trend positive">活跃度提升</div>
</div>
<div class="stat-card">
<div class="stat-value">92.5%</div>
<div class="muted">客户满意度</div>
<div class="stat-trend positive">表现优秀</div>
</div>
</div>
<!-- 客户列表 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">客户列表</h2>
<div class="panel-actions">
<button class="btn primary">
<span class="btn-icon"></span>
新增客户
</button>
</div>
</div>
<!-- 搜索筛选 -->
<div class="toolbar">
<div class="filter-group">
<select v-model="selectedLevel" class="select">
<option v-for="option in levelOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
<select v-model="selectedStatus" class="select">
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
</div>
<div class="search-group">
<input v-model="searchQuery" class="input" placeholder="搜索姓名、电话或标签..." />
<button class="btn primary">搜索</button>
</div>
<div class="action-group">
<button class="btn secondary">重置</button>
<button class="btn secondary">导出列表</button>
</div>
</div>
<!-- 客户表格 -->
<div class="table-container">
<table class="table">
<thead>
<tr>
<th style="width: 120px;">客户信息</th>
<th style="width: 100px;">联系方式</th>
<th style="width: 80px;">优先级</th>
<th style="width: 80px;">健康评分</th>
<th style="width: 120px;">消费数据</th>
<th style="width: 150px;">标签</th>
<th style="width: 100px;">状态</th>
<th style="width: 120px;">注册时间</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in customers" :key="item.id" class="customer-row">
<td>
<div class="customer-info">
<div class="customer-avatar">{{ item.avatar }}</div>
<div class="customer-details">
<div class="customer-name">{{ item.name }}</div>
<div class="customer-meta">
<span class="last-contact">最后联系: {{ item.lastContact }}</span>
</div>
</div>
</div>
</td>
<td>
<div class="contact-info">
<div class="phone-number">{{ item.phone }}</div>
<div class="contact-status" :class="item.status">
{{ item.status === 'active' ? '可联系' : '待跟进' }}
</div>
</div>
</td>
<td>
<span class="priority-tag" :class="getLevelInfo(item.level).color">
<span class="priority-icon">{{ getLevelInfo(item.level).icon }}</span>
{{ getLevelInfo(item.level).label }}
</span>
</td>
<td>
<div class="health-score" :class="getHealthScoreInfo(item.healthScore).color">
<div class="score-value">{{ item.healthScore }}</div>
<div class="score-label">{{ getHealthScoreInfo(item.healthScore).label }}</div>
</div>
</td>
<td>
<div class="purchase-info">
<div class="purchase-item">
<span class="purchase-label">订单数:</span>
<span class="purchase-value">{{ item.orders }}</span>
</div>
<div class="purchase-item">
<span class="purchase-label">总消费:</span>
<span class="purchase-value">¥{{ item.totalSpent }}</span>
</div>
</div>
</td>
<td>
<div class="tags-container">
<span v-for="tag in item.tags" :key="tag" class="tag"
:class="{
'blue': tag.includes('设备') || tag.includes('新注册'),
'green': tag.includes('糖尿病') || tag.includes('活跃'),
'orange': tag.includes('重点') || tag.includes('咨询'),
'red': tag.includes('VIP')
}">
{{ tag }}
</span>
</div>
</td>
<td>
<span class="status-tag" :class="getStatusInfo(item.status).color">
<span class="status-icon">{{ getStatusInfo(item.status).icon }}</span>
{{ getStatusInfo(item.status).label }}
</span>
</td>
<td>
<div class="date-info">
<div class="register-date">{{ item.date }}</div>
<div class="days-ago muted">{{ Math.ceil((new Date() - new Date(item.date)) / (1000 * 60 * 60 * 24)) }} 天前</div>
</div>
</td>
<td>
<div class="action-buttons">
<button @click="contactCustomer(item.id)" class="btn primary small">
<span class="btn-icon">📞</span>
联系
</button>
<button @click="viewDetails(item.id)" class="btn secondary small">
<span class="btn-icon">👁</span>
详情
</button>
<button class="btn secondary small">
<span class="btn-icon">📝</span>
记录
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="table-footer">
<div class="pagination-info">
显示 1-4 4 条记录
</div>
<div class="pagination">
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button></button>
</div>
</div>
</div>
<!-- 客户洞察 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">客户洞察</h3>
<div class="time-filter">
<select class="select small">
<option>今日</option>
<option>本周</option>
<option selected>本月</option>
<option>全年</option>
</select>
</div>
</div>
<div class="insights-grid">
<div class="insight-card">
<div class="insight-header">
<div class="insight-icon">📈</div>
<div class="insight-title">客户增长</div>
</div>
<div class="insight-content">
<div class="growth-stats">
<div class="growth-item">
<div class="growth-value">+24%</div>
<div class="growth-label">新客户增长</div>
</div>
<div class="growth-item">
<div class="growth-value">+15%</div>
<div class="growth-label">活跃度提升</div>
</div>
</div>
</div>
</div>
<div class="insight-card">
<div class="insight-header">
<div class="insight-icon">💰</div>
<div class="insight-title">消费分析</div>
</div>
<div class="insight-content">
<div class="spending-stats">
<div class="spending-item">
<div class="spending-value">¥12,450</div>
<div class="spending-label">月总消费</div>
</div>
<div class="spending-item">
<div class="spending-value">¥510</div>
<div class="spending-label">客均消费</div>
</div>
</div>
</div>
</div>
<div class="insight-card">
<div class="insight-header">
<div class="insight-icon">👥</div>
<div class="insight-title">客户分布</div>
</div>
<div class="insight-content">
<div class="distribution-stats">
<div class="distribution-item">
<div class="distribution-value">68%</div>
<div class="distribution-label">糖尿病客户</div>
</div>
<div class="distribution-item">
<div class="distribution-value">45%</div>
<div class="distribution-label">设备用户</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 快速行动 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">快速行动</h3>
</div>
<div class="quick-actions">
<button class="quick-action-btn">
<span class="action-icon">📧</span>
<span class="action-text">批量消息</span>
</button>
<button class="quick-action-btn">
<span class="action-icon">📋</span>
<span class="action-text">健康回访</span>
</button>
<button class="quick-action-btn">
<span class="action-icon">🎯</span>
<span class="action-text">精准营销</span>
</button>
<button class="quick-action-btn">
<span class="action-icon">📊</span>
<span class="action-text">数据分析</span>
</button>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.customer-row {
transition: all 0.3s ease;
}
.customer-row:hover {
background: var(--color-primary-50) !important;
}
.customer-info {
display: flex;
align-items: center;
gap: var(--space-3);
}
.customer-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--color-gray-200);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
flex-shrink: 0;
}
.customer-details {
flex: 1;
min-width: 0;
}
.customer-name {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-1);
font-size: var(--text-sm);
}
.customer-meta {
font-size: var(--text-xs);
color: var(--color-gray-500);
}
.last-contact {
display: block;
}
.contact-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.phone-number {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.contact-status {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
font-weight: 500;
text-align: center;
}
.contact-status.active {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-600);
}
.contact-status.inactive {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-600);
}
.priority-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.priority-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.priority-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.priority-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.priority-icon {
font-size: var(--text-xs);
}
.health-score {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
padding: var(--space-2);
border-radius: var(--radius-lg);
text-align: center;
}
.health-score.green {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.health-score.orange {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.2);
}
.health-score.red {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.score-value {
font-size: var(--text-lg);
font-weight: 800;
line-height: 1;
}
.health-score.green .score-value {
color: var(--color-success-600);
}
.health-score.orange .score-value {
color: var(--color-warning-600);
}
.health-score.red .score-value {
color: var(--color-error-600);
}
.score-label {
font-size: var(--text-xs);
font-weight: 600;
}
.health-score.green .score-label {
color: var(--color-success-700);
}
.health-score.orange .score-label {
color: var(--color-warning-700);
}
.health-score.red .score-label {
color: var(--color-error-700);
}
.purchase-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.purchase-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
}
.purchase-label {
font-size: var(--text-xs);
color: var(--color-gray-600);
}
.purchase-value {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-gray-900);
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
max-width: 120px;
}
.status-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.status-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.status-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.status-icon {
font-size: var(--text-xs);
}
.date-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.register-date {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.days-ago {
font-size: var(--text-xs);
}
.action-buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.insights-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-5);
padding: var(--space-5);
}
.insight-card {
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
transition: all 0.3s ease;
}
.insight-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary-300);
}
.insight-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.insight-icon {
font-size: 1.5rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
.insight-title {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-base);
}
.insight-content {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.growth-stats,
.spending-stats,
.distribution-stats {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.growth-item,
.spending-item,
.distribution-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-2);
background: #fff;
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
}
.growth-value,
.spending-value,
.distribution-value {
font-weight: 700;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.growth-label,
.spending-label,
.distribution-label {
font-size: var(--text-xs);
color: var(--color-gray-600);
font-weight: 500;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: var(--space-4);
padding: var(--space-5);
}
.quick-action-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-4);
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.quick-action-btn:hover {
background: var(--color-primary-50);
border-color: var(--color-primary-300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.action-icon {
font-size: 2rem;
margin-bottom: var(--space-1);
}
.action-text {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-gray-700);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
gap: var(--space-4);
}
.filter-group,
.search-group,
.action-group {
width: 100%;
}
.table-container {
overflow-x: auto;
}
.insights-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
.customer-info {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
.insights-grid {
grid-template-columns: 1fr;
}
.quick-actions {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr 1fr;
}
.quick-actions {
grid-template-columns: 1fr;
}
.tags-container {
max-width: 80px;
}
}
</style>

@ -0,0 +1,720 @@
<script setup>
import { computed, ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const profile = {
name: '张三',
phone: '1234567890',
gender: '男',
staffNo: '123456',
birthday: '2001-12-20',
lastLogin: '2024-12-24 23:12:00',
avatar: 'https://images.unsplash.com/photo-1504595403659-9088ce801e29?auto=format&fit=crop&w=400&q=80',
}
const monthlyWork = [100, 140, 110, 200, 120, 150]
const monthlyWorkSecondary = [150, 120, 100, 190, 160, 110]
const months = ['1月', '2月', '3月', '4月', '5月', '6月']
const isMobile = ref(window.innerWidth < 768)
const updateViewport = () => {
isMobile.value = window.innerWidth < 768
}
//
window.addEventListener('resize', updateViewport)
const lineChartConfig = computed(() => {
const width = isMobile.value ? 300 : 520
const height = isMobile.value ? 180 : 260
const max = Math.max(...monthlyWork, ...monthlyWorkSecondary) * 1.1
const step = width / (monthlyWork.length - 1)
const toPoints = (data) =>
data
.map((v, i) => {
const x = i * step
const y = height - (v / max) * height + 10
return `${x},${y}`
})
.join(' ')
return {
width,
height,
primaryPath: toPoints(monthlyWork),
secondaryPath: toPoints(monthlyWorkSecondary),
}
})
</script>
<template>
<LayoutFrame title="个人主页">
<section class="app-card info-card">
<div class="panel-header">
<h2 class="panel-title">基本资料</h2>
<button class="btn secondary small">
<span class="btn-icon"></span>
<span class="btn-text">编辑资料</span>
</button>
</div>
<div class="info-content">
<div class="info-left">
<div class="avatar-container">
<img :src="profile.avatar" class="portrait" alt="用户头像" />
<div class="avatar-badge online"></div>
</div>
<div class="avatar-actions">
<button class="btn secondary small full-width">
<span class="btn-icon">🔄</span>
<span class="btn-text">更换头像</span>
</button>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">
<span class="info-icon">👤</span>
<span class="info-label-text">姓名</span>
</span>
<span class="info-value">{{ profile.name }}</span>
</div>
<div class="info-item">
<span class="info-label">
<span class="info-icon">📱</span>
<span class="info-label-text">手机号</span>
</span>
<span class="info-value">{{ profile.phone }}</span>
</div>
<div class="info-item">
<span class="info-label">
<span class="info-icon"></span>
<span class="info-label-text">性别</span>
</span>
<span class="info-value">{{ profile.gender }}</span>
</div>
<div class="info-item">
<span class="info-label">
<span class="info-icon">🔢</span>
<span class="info-label-text">工号</span>
</span>
<span class="info-value">{{ profile.staffNo }}</span>
</div>
<div class="info-item">
<span class="info-label">
<span class="info-icon">🎂</span>
<span class="info-label-text">生日</span>
</span>
<span class="info-value">{{ profile.birthday }}</span>
</div>
<div class="info-item">
<span class="info-label">
<span class="info-icon">🕒</span>
<span class="info-label-text">最近登录</span>
</span>
<span class="info-value">{{ profile.lastLogin }}</span>
</div>
</div>
</div>
</section>
<section class="charts-grid">
<div class="app-card chart-card">
<div class="panel-header">
<h3 class="panel-title">工作量统计</h3>
<div class="chart-legend">
<div class="legend-item">
<span class="legend-dot primary"></span>
<span class="legend-text">本月</span>
</div>
<div class="legend-item">
<span class="legend-dot secondary"></span>
<span class="legend-text">上月</span>
</div>
</div>
</div>
<div class="chart-content">
<svg
class="line-chart"
:width="lineChartConfig.width"
:height="lineChartConfig.height + 30"
:viewBox="`0 0 ${lineChartConfig.width} ${lineChartConfig.height + 30}`"
>
<!-- 网格线 -->
<g class="grid-lines">
<line x1="0" y1="50" :x2="lineChartConfig.width" y2="50" stroke="var(--color-gray-200)" stroke-width="1" />
<line x1="0" y1="150" :x2="lineChartConfig.width" y2="150" stroke="var(--color-gray-200)" stroke-width="1" />
<line x1="0" y1="250" :x2="lineChartConfig.width" y2="250" stroke="var(--color-gray-200)" stroke-width="1" />
</g>
<!-- 数据线 -->
<polyline
:points="lineChartConfig.secondaryPath"
fill="none"
stroke="var(--color-primary-300)"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<polyline
:points="lineChartConfig.primaryPath"
fill="none"
stroke="var(--color-primary-500)"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- 数据点 -->
<g class="data-points">
<circle
v-for="(point, index) in lineChartConfig.primaryPath.split(' ')"
:key="'primary-' + index"
:cx="point.split(',')[0]"
:cy="point.split(',')[1]"
r="4"
fill="var(--color-primary-500)"
stroke="#fff"
stroke-width="2"
/>
</g>
<!-- 坐标轴标签 -->
<g class="axis-labels">
<text v-for="(label, idx) in months" :key="label"
:x="(idx / (months.length - 1)) * lineChartConfig.width"
y="290" class="axis-text">
{{ label }}
</text>
</g>
</svg>
</div>
</div>
<div class="app-card chart-card">
<div class="panel-header">
<h3 class="panel-title">今日工作完成度</h3>
<span class="chart-subtitle">实时更新</span>
</div>
<div class="chart-content">
<div class="pie-legend-group">
<div class="pie-legend">
<span class="legend-dot primary"></span>
<span class="legend-text">已完成</span>
<span class="legend-value">60%</span>
</div>
<div class="pie-legend">
<span class="legend-dot secondary"></span>
<span class="legend-text">未完成</span>
<span class="legend-value">40%</span>
</div>
</div>
<div class="pie-chart-container">
<div class="pie-fill"></div>
<div class="pie-hole">
<div class="pie-value">60%</div>
<div class="pie-label">完成率</div>
</div>
</div>
<div class="completion-stats">
<div class="stat-item">
<div class="stat-number">18</div>
<div class="stat-label">已完成任务</div>
</div>
<div class="stat-item">
<div class="stat-number">12</div>
<div class="stat-label">待完成任务</div>
</div>
</div>
</div>
</div>
</section>
</LayoutFrame>
</template>
<style scoped>
.info-card {
padding: 0;
}
.info-content {
display: grid;
grid-template-columns: 240px 1fr;
gap: var(--space-8);
padding: var(--space-6);
}
.info-left {
display: flex;
flex-direction: column;
gap: var(--space-4);
align-items: center;
}
.avatar-container {
position: relative;
display: inline-block;
}
.portrait {
width: 160px;
height: 160px;
object-fit: cover;
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-lg);
border: 4px solid #fff;
}
.avatar-badge {
position: absolute;
bottom: 12px;
right: 12px;
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid #fff;
}
.avatar-badge.online {
background: var(--color-success-500);
}
.avatar-actions {
width: 100%;
}
.btn.full-width {
width: 100%;
justify-content: center;
}
.btn-text {
display: inline;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-4);
align-content: start;
}
.info-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
transition: all 0.2s ease;
}
.info-item:hover {
background: var(--color-primary-50);
border-color: var(--color-primary-200);
}
.info-label {
color: var(--color-gray-600);
font-weight: 500;
min-width: 100px;
display: flex;
align-items: center;
gap: var(--space-2);
}
.info-label-text {
display: inline;
}
.info-icon {
font-size: var(--text-sm);
opacity: 0.7;
}
.info-value {
color: var(--color-gray-900);
font-weight: 600;
flex: 1;
}
.charts-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-6);
}
.chart-card {
padding: 0;
}
.chart-content {
padding: var(--space-6);
}
.line-chart {
width: 100%;
height: auto;
}
.chart-legend {
display: flex;
gap: var(--space-4);
}
.legend-item {
display: flex;
align-items: center;
gap: var(--space-2);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.legend-dot.primary {
background: var(--color-primary-500);
}
.legend-dot.secondary {
background: var(--color-primary-300);
}
.legend-text {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
}
.axis-text {
fill: var(--color-gray-500);
font-size: var(--text-xs);
text-anchor: middle;
font-weight: 500;
}
.chart-subtitle {
font-size: var(--text-sm);
color: var(--color-gray-500);
font-weight: 500;
}
.pie-legend-group {
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-bottom: var(--space-6);
}
.pie-legend {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
background: var(--color-gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
}
.legend-value {
font-weight: 600;
color: var(--color-gray-700);
}
.pie-chart-container {
position: relative;
width: 200px;
height: 200px;
margin: 0 auto var(--space-6);
}
.pie-fill {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(
var(--color-primary-500) 0% 60%,
var(--color-primary-300) 60% 100%
);
box-shadow: var(--shadow-lg);
position: relative;
}
.pie-fill::before {
content: '';
position: absolute;
top: 8px;
left: 8px;
right: 8px;
bottom: 8px;
background: conic-gradient(
var(--color-primary-400) 0% 60%,
var(--color-primary-200) 60% 100%
);
border-radius: 50%;
filter: blur(8px);
opacity: 0.3;
}
.pie-hole {
position: absolute;
inset: 20px;
border-radius: 50%;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-1);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
}
.pie-value {
color: var(--color-primary-600);
font-size: var(--text-2xl);
font-weight: 800;
line-height: 1;
}
.pie-label {
color: var(--color-gray-500);
font-size: var(--text-sm);
font-weight: 500;
}
.completion-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
text-align: center;
}
.stat-item {
padding: var(--space-3);
background: var(--color-gray-50);
border-radius: var(--radius-lg);
border: 1px solid var(--color-gray-200);
}
.stat-number {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-gray-900);
line-height: 1;
margin-bottom: var(--space-1);
}
.stat-label {
font-size: var(--text-xs);
color: var(--color-gray-500);
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.info-content {
grid-template-columns: 200px 1fr;
gap: var(--space-6);
}
.portrait {
width: 140px;
height: 140px;
}
}
@media (max-width: 1024px) {
.charts-grid {
grid-template-columns: 1fr;
gap: var(--space-4);
}
.info-content {
grid-template-columns: 1fr;
text-align: center;
gap: var(--space-6);
}
.info-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.info-content,
.chart-content {
padding: var(--space-4);
}
.info-grid {
grid-template-columns: 1fr;
gap: var(--space-3);
}
.info-item {
flex-direction: column;
gap: var(--space-2);
text-align: center;
padding: var(--space-3);
}
.info-label {
min-width: auto;
justify-content: center;
}
.portrait {
width: 120px;
height: 120px;
}
.pie-chart-container {
width: 160px;
height: 160px;
}
.pie-fill {
width: 160px;
height: 160px;
}
.pie-hole {
inset: 16px;
}
.pie-value {
font-size: var(--text-xl);
}
.completion-stats {
grid-template-columns: 1fr;
gap: var(--space-3);
}
.panel-header {
flex-direction: column;
gap: var(--space-3);
align-items: stretch;
}
.chart-legend {
justify-content: center;
}
}
@media (max-width: 640px) {
.app-card {
margin: var(--space-2);
}
.info-content {
padding: var(--space-3);
}
.chart-content {
padding: var(--space-3);
}
.pie-chart-container {
width: 140px;
height: 140px;
}
.pie-fill {
width: 140px;
height: 140px;
}
.pie-hole {
inset: 14px;
}
.pie-value {
font-size: var(--text-lg);
}
.btn .btn-text {
display: none;
}
.btn .btn-icon {
margin-right: 0;
}
}
@media (max-width: 480px) {
.info-content {
padding: var(--space-2);
}
.chart-content {
padding: var(--space-2);
}
.portrait {
width: 100px;
height: 100px;
}
.info-item {
padding: var(--space-2);
}
.pie-chart-container {
width: 120px;
height: 120px;
}
.pie-fill {
width: 120px;
height: 120px;
}
.pie-hole {
inset: 12px;
}
.stat-number {
font-size: var(--text-lg);
}
}
/* 横屏手机优化 */
@media (max-width: 768px) and (orientation: landscape) {
.info-content {
grid-template-columns: 120px 1fr;
text-align: left;
gap: var(--space-4);
}
.info-item {
flex-direction: row;
text-align: left;
}
.info-label {
justify-content: flex-start;
}
}
/* 高DPI屏幕优化 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.portrait {
border-width: 2px;
}
.avatar-badge {
border-width: 2px;
}
}
</style>

@ -0,0 +1,930 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const devices = ref([
{
id: 'D-1001',
type: '血糖仪',
model: 'GlucoCheck Pro',
status: 'online',
owner: '李雷',
phone: '13800001111',
version: 'v1.2.0',
lastActive: '2分钟前',
battery: 85,
signal: 'strong',
dataCount: 1245,
lastSync: '今天 08:30'
},
{
id: 'D-1002',
type: '手环',
model: 'HealthBand 3',
status: 'offline',
owner: '韩梅梅',
phone: '13900002222',
version: 'v1.1.5',
lastActive: '3天前',
battery: 15,
signal: 'weak',
dataCount: 892,
lastSync: '3天前'
},
{
id: 'D-1003',
type: '血压计',
model: 'BP Monitor Plus',
status: 'online',
owner: '王强',
phone: '13700003333',
version: 'v1.0.9',
lastActive: '5分钟前',
battery: 92,
signal: 'medium',
dataCount: 567,
lastSync: '今天 09:15'
},
{
id: 'D-1004',
type: '体重秤',
model: 'SmartScale X1',
status: 'online',
owner: '赵小云',
phone: '13600004444',
version: 'v2.1.3',
lastActive: '1小时前',
battery: 78,
signal: 'strong',
dataCount: 234,
lastSync: '今天 07:45'
}
])
const searchQuery = ref('')
const selectedStatus = ref('all')
const selectedType = ref('all')
const statusOptions = [
{ value: 'all', label: '全部状态', icon: '📊' },
{ value: 'online', label: '在线', icon: '🟢' },
{ value: 'offline', label: '离线', icon: '🔴' },
{ value: 'error', label: '异常', icon: '🟡' }
]
const typeOptions = [
{ value: 'all', label: '全部类型', icon: '📱' },
{ value: 'glucometer', label: '血糖仪', icon: '🩸' },
{ value: 'band', label: '手环', icon: '⌚' },
{ value: 'bp', label: '血压计', icon: '💓' },
{ value: 'scale', label: '体重秤', icon: '⚖️' }
]
const getStatusInfo = (status) => {
const statusMap = {
online: { label: '在线', color: 'green', icon: '🟢' },
offline: { label: '离线', color: 'red', icon: '🔴' },
error: { label: '异常', color: 'orange', icon: '🟡' }
}
return statusMap[status] || statusMap.offline
}
const getTypeInfo = (type) => {
const typeMap = {
'血糖仪': { icon: '🩸', color: 'red' },
'手环': { icon: '⌚', color: 'blue' },
'血压计': { icon: '💓', color: 'green' },
'体重秤': { icon: '⚖️', color: 'purple' }
}
return typeMap[type] || { icon: '📱', color: 'gray' }
}
const getBatteryInfo = (level) => {
if (level >= 80) return { color: 'green', icon: '🔋' }
if (level >= 20) return { color: 'orange', icon: '🪫' }
return { color: 'red', icon: '🪫' }
}
const getSignalInfo = (signal) => {
const signalMap = {
strong: { label: '强', color: 'green', icon: '📶' },
medium: { label: '中', color: 'orange', icon: '📶' },
weak: { label: '弱', color: 'red', icon: '📶' }
}
return signalMap[signal] || signalMap.weak
}
const sendCommand = (deviceId, command) => {
console.log(`向设备 ${deviceId} 发送命令: ${command}`)
}
const updateFirmware = (deviceId) => {
console.log(`更新设备 ${deviceId} 固件`)
}
</script>
<template>
<LayoutFrame title="设备管理">
<!-- 统计概览 -->
<div class="cards-grid">
<div class="stat-card">
<div class="stat-value">156</div>
<div class="muted">总设备数</div>
<div class="stat-trend positive">较上月 +12%</div>
</div>
<div class="stat-card success">
<div class="stat-value">142</div>
<div class="muted">在线设备</div>
<div class="stat-desc">91% 在线率</div>
</div>
<div class="stat-card warning">
<div class="stat-value">14</div>
<div class="muted">离线设备</div>
<div class="stat-trend warning">需关注</div>
</div>
<div class="stat-card">
<div class="stat-value">98.2%</div>
<div class="muted">设备稳定性</div>
<div class="stat-trend positive">表现优秀</div>
</div>
</div>
<!-- 设备列表 -->
<div class="app-card">
<div class="panel-header">
<h2 class="panel-title">设备列表</h2>
<div class="panel-actions">
<button class="btn primary">
<span class="btn-icon"></span>
添加设备
</button>
</div>
</div>
<!-- 搜索筛选 -->
<div class="toolbar">
<div class="filter-group">
<select v-model="selectedStatus" class="select">
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
<select v-model="selectedType" class="select">
<option v-for="option in typeOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
</div>
<div class="search-group">
<input v-model="searchQuery" class="input" placeholder="搜索设备编号、型号或用户..." />
<button class="btn primary">搜索</button>
</div>
<div class="action-group">
<button class="btn secondary">重置</button>
<button class="btn secondary">导出数据</button>
</div>
</div>
<!-- 设备表格 -->
<div class="table-container">
<table class="table">
<thead>
<tr>
<th style="width: 120px;">设备信息</th>
<th style="width: 100px;">类型</th>
<th style="width: 100px;">状态</th>
<th style="width: 80px;">电池</th>
<th style="width: 80px;">信号</th>
<th style="width: 100px;">绑定用户</th>
<th style="width: 100px;">数据记录</th>
<th style="width: 120px;">最后同步</th>
<th style="width: 150px;">固件版本</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in devices" :key="item.id" class="device-row">
<td>
<div class="device-info">
<div class="device-icon" :style="{ color: getTypeInfo(item.type).color }">
{{ getTypeInfo(item.type).icon }}
</div>
<div class="device-details">
<div class="device-id">{{ item.id }}</div>
<div class="device-model">{{ item.model }}</div>
</div>
</div>
</td>
<td>
<span class="type-tag" :style="{
backgroundColor: `var(--color-${getTypeInfo(item.type).color}-50)`,
color: `var(--color-${getTypeInfo(item.type).color}-700)`,
borderColor: `var(--color-${getTypeInfo(item.type).color}-200)`
}">
{{ item.type }}
</span>
</td>
<td>
<div class="status-indicator">
<span class="status-dot" :class="getStatusInfo(item.status).color"></span>
<span class="status-tag" :class="getStatusInfo(item.status).color">
{{ getStatusInfo(item.status).label }}
</span>
</div>
</td>
<td>
<div class="battery-info" :class="getBatteryInfo(item.battery).color">
<span class="battery-icon">{{ getBatteryInfo(item.battery).icon }}</span>
<span class="battery-level">{{ item.battery }}%</span>
</div>
</td>
<td>
<div class="signal-info" :class="getSignalInfo(item.signal).color">
<span class="signal-icon">{{ getSignalInfo(item.signal).icon }}</span>
<span class="signal-level">{{ getSignalInfo(item.signal).label }}</span>
</div>
</td>
<td>
<div class="owner-info">
<div class="owner-name">{{ item.owner }}</div>
<div class="owner-phone muted">{{ item.phone }}</div>
</div>
</td>
<td>
<div class="data-count">
<div class="count-value">{{ item.dataCount }}</div>
<div class="count-label">条记录</div>
</div>
</td>
<td>
<div class="sync-info">
<div class="sync-time">{{ item.lastSync }}</div>
<div class="last-active muted">{{ item.lastActive }}</div>
</div>
</td>
<td>
<div class="version-info">
<span class="version-tag">{{ item.version }}</span>
<div class="update-available" v-if="item.version < 'v2.0.0'">
可升级
</div>
</div>
</td>
<td>
<div class="action-buttons">
<button class="btn secondary small">
<span class="btn-icon">👁</span>
详情
</button>
<button @click="updateFirmware(item.id)" class="btn primary small"
v-if="item.version < 'v2.0.0'">
<span class="btn-icon">🔄</span>
升级
</button>
<button class="btn secondary small">
<span class="btn-icon">📊</span>
数据
</button>
<button class="btn secondary small">
<span class="btn-icon"></span>
设置
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="table-footer">
<div class="pagination-info">
显示 1-4 4 条记录
</div>
<div class="pagination">
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button></button>
</div>
</div>
</div>
<!-- 设备监控 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">设备监控</h3>
<div class="time-filter">
<select class="select small">
<option>实时</option>
<option>今日</option>
<option selected>本周</option>
<option>本月</option>
</select>
</div>
</div>
<div class="monitoring-grid">
<div class="monitoring-chart">
<div class="chart-title">设备在线率趋势</div>
<div class="chart-content">
<div class="trend-line">
<div class="trend-point" style="left: 0%; background: var(--color-success-500);"></div>
<div class="trend-point" style="left: 25%; background: var(--color-success-500);"></div>
<div class="trend-point" style="left: 50%; background: var(--color-warning-500);"></div>
<div class="trend-point" style="left: 75%; background: var(--color-success-500);"></div>
<div class="trend-point" style="left: 100%; background: var(--color-success-500);"></div>
</div>
<div class="chart-labels">
<span>周一</span>
<span>周二</span>
<span>周三</span>
<span>周四</span>
<span>周五</span>
</div>
</div>
</div>
<div class="monitoring-stats">
<div class="stat-item">
<div class="stat-icon">📈</div>
<div class="stat-content">
<div class="stat-value">94.2%</div>
<div class="stat-label">平均在线率</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"></div>
<div class="stat-content">
<div class="stat-value">2.3s</div>
<div class="stat-label">平均响应时间</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon">🔄</div>
<div class="stat-content">
<div class="stat-value">156</div>
<div class="stat-label">今日同步次数</div>
</div>
</div>
</div>
</div>
</div>
<!-- 批量操作 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">批量操作</h3>
</div>
<div class="batch-actions">
<div class="action-group">
<label class="action-label">选择操作:</label>
<select class="select">
<option>批量固件升级</option>
<option>批量重启设备</option>
<option>批量数据同步</option>
<option>批量导出数据</option>
</select>
<button class="btn primary">执行操作</button>
</div>
<div class="action-group">
<label class="action-label">自动维护:</label>
<div class="maintenance-settings">
<label class="checkbox">
<input type="checkbox" checked />
<span class="checkmark"></span>
自动固件升级
</label>
<label class="checkbox">
<input type="checkbox" checked />
<span class="checkmark"></span>
自动数据备份
</label>
<label class="checkbox">
<input type="checkbox" />
<span class="checkmark"></span>
离线设备提醒
</label>
</div>
</div>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.device-row {
transition: all 0.3s ease;
}
.device-row:hover {
background: var(--color-primary-50) !important;
}
.device-info {
display: flex;
align-items: center;
gap: var(--space-3);
}
.device-icon {
font-size: 1.5rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-gray-100);
border-radius: var(--radius-lg);
flex-shrink: 0;
}
.device-details {
flex: 1;
min-width: 0;
}
.device-id {
font-family: 'Monaco', 'Consolas', monospace;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-1);
font-size: var(--text-sm);
}
.device-model {
font-size: var(--text-xs);
color: var(--color-gray-500);
}
.type-tag {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-indicator {
display: flex;
align-items: center;
gap: var(--space-2);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.green {
background: var(--color-success-500);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.status-dot.red {
background: var(--color-error-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
.status-dot.orange {
background: var(--color-warning-500);
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
.status-tag {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.status-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.status-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.battery-info {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 600;
}
.battery-info.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.battery-info.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border: 1px solid rgba(245, 158, 11, 0.2);
}
.battery-info.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.battery-icon {
font-size: var(--text-xs);
}
.signal-info {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 600;
}
.signal-info.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.signal-info.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border: 1px solid rgba(245, 158, 11, 0.2);
}
.signal-info.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.signal-icon {
font-size: var(--text-xs);
}
.owner-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.owner-name {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.owner-phone {
font-size: var(--text-xs);
}
.data-count {
display: flex;
align-items: baseline;
gap: var(--space-1);
}
.count-value {
font-weight: 700;
color: var(--color-gray-900);
font-size: var(--text-lg);
}
.count-label {
font-size: var(--text-xs);
color: var(--color-gray-500);
}
.sync-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.sync-time {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.last-active {
font-size: var(--text-xs);
}
.version-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
align-items: flex-start;
}
.version-tag {
padding: var(--space-1) var(--space-2);
background: var(--color-gray-100);
color: var(--color-gray-700);
border-radius: var(--radius);
font-size: var(--text-xs);
font-weight: 600;
font-family: 'Monaco', 'Consolas', monospace;
border: 1px solid var(--color-gray-200);
}
.update-available {
font-size: var(--text-xs);
color: var(--color-primary-600);
font-weight: 600;
padding: var(--space-1) var(--space-2);
background: rgba(59, 130, 246, 0.1);
border-radius: var(--radius);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.action-buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.monitoring-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-6);
padding: var(--space-5);
}
.monitoring-chart {
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
}
.chart-title {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-4);
font-size: var(--text-lg);
}
.chart-content {
position: relative;
height: 80px;
}
.trend-line {
position: relative;
height: 40px;
margin: 20px 0;
border-bottom: 2px solid var(--color-gray-300);
}
.trend-point {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
transform: translate(-50%, 50%);
border: 2px solid white;
box-shadow: var(--shadow-sm);
}
.trend-point::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
border-radius: 50%;
transform: translate(-50%, -50%);
opacity: 0.2;
}
.trend-point[style*="background: var(--color-success-500)"]::before {
background: var(--color-success-500);
}
.trend-point[style*="background: var(--color-warning-500)"]::before {
background: var(--color-warning-500);
}
.chart-labels {
display: flex;
justify-content: space-between;
margin-top: var(--space-2);
}
.chart-labels span {
font-size: var(--text-xs);
color: var(--color-gray-500);
font-weight: 500;
}
.monitoring-stats {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.stat-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
transition: all 0.3s ease;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary-300);
}
.stat-icon {
font-size: 1.5rem;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
.stat-content {
flex: 1;
}
.stat-value {
font-size: var(--text-xl);
font-weight: 800;
color: var(--color-gray-900);
line-height: 1;
margin-bottom: var(--space-1);
}
.stat-label {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
}
.batch-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-6);
padding: var(--space-5);
}
.action-group {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.action-label {
font-weight: 600;
color: var(--color-gray-700);
font-size: var(--text-sm);
}
.maintenance-settings {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
font-size: var(--text-sm);
color: var(--color-gray-700);
}
.checkbox input {
display: none;
}
.checkmark {
width: 16px;
height: 16px;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-sm);
position: relative;
transition: all 0.2s ease;
}
.checkbox input:checked + .checkmark {
background: var(--color-primary-500);
border-color: var(--color-primary-500);
}
.checkbox input:checked + .checkmark::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 10px;
font-weight: bold;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
gap: var(--space-4);
}
.filter-group,
.search-group,
.action-group {
width: 100%;
}
.table-container {
overflow-x: auto;
}
.monitoring-grid {
grid-template-columns: 1fr;
gap: var(--space-4);
}
.batch-actions {
grid-template-columns: 1fr;
gap: var(--space-4);
}
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
.device-info {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
.stat-item {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr 1fr;
}
.version-info {
align-items: center;
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,238 @@
<script setup>
import { ref } from "vue"
import { useRouter } from "vue-router"
const router = useRouter()
const form = ref({ account: "", password: "" })
const error = ref("")
const handleSubmit = () => {
if (!form.value.account || !form.value.password) {
error.value = "请输入账户和密码"
return
}
error.value = ""
router.push("/home")
}
</script>
<template>
<div class="login-page">
<div class="bg-wave bg-left"></div>
<div class="bg-wave bg-right"></div>
<div class="login-card app-card">
<h1>登录</h1>
<div class="form">
<label class="field">
<span class="field-icon">👤</span>
<input v-model="form.account" placeholder="请输入用户账号/手机号" />
</label>
<label class="field">
<span class="field-icon">🔒</span>
<input v-model="form.password" type="password" placeholder="请输入密码" />
</label>
<div class="actions">
<button class="primary" @click="handleSubmit"></button>
</div>
<p v-if="error" class="error">{{ error }}</p>
<div class="links">
<a href="#">忘记密码?</a>
<div>
<span>没有账号?</span>
<a class="link" href="#">立即注册</a>
</div>
</div>
</div>
</div>
<div class="decor">
<div class="badge"></div>
<div class="pill pill-main"></div>
<div class="pill pill-secondary"></div>
<div class="pulse"></div>
</div>
</div>
</template>
<style scoped>
.login-page {
position: relative;
min-height: 100vh;
display: grid;
place-items: center;
overflow: hidden;
background: linear-gradient(120deg, #2d8bff 0%, #2e5bf3 50%, #f9fbff 50%);
}
.login-card {
position: relative;
width: 560px;
padding: 32px 46px 36px;
border-radius: 16px;
z-index: 2;
}
h1 {
margin: 0 0 26px;
font-size: 30px;
color: #1d2d3d;
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
.field {
display: flex;
align-items: center;
padding: 12px 14px;
border: 1px solid #e4e9f2;
border-radius: 10px;
background: #f9fbff;
gap: 10px;
transition: border 0.2s ease, box-shadow 0.2s ease;
}
.field:focus-within {
border-color: #4d7bff;
box-shadow: 0 8px 20px rgba(77, 123, 255, 0.15);
}
.field-icon {
font-size: 18px;
opacity: 0.65;
}
input {
border: none;
outline: none;
width: 100%;
padding: 6px 4px;
background: transparent;
font-size: 15px;
}
.actions {
margin-top: 4px;
}
.primary {
width: 100%;
border: none;
border-radius: 10px;
padding: 12px;
background: linear-gradient(90deg, #2e66f3 0%, #2bb5ff 100%);
color: #fff;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.2s ease;
}
.primary:hover {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(46, 102, 243, 0.35);
}
.links {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
color: #78889c;
margin-top: 4px;
}
.link {
color: #2e66f3;
margin-left: 6px;
}
.error {
margin: 0;
color: #e64d3d;
font-size: 13px;
}
.decor {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.badge {
position: absolute;
top: 22%;
right: 22%;
width: 70px;
height: 70px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.86);
color: #2f80ff;
display: grid;
place-items: center;
font-size: 34px;
font-weight: 700;
box-shadow: 0 16px 40px rgba(46, 102, 243, 0.18);
}
.pill {
position: absolute;
border-radius: 26px;
filter: drop-shadow(0 16px 32px rgba(0, 0, 0, 0.12));
}
.pill-main {
width: 120px;
height: 46px;
background: linear-gradient(110deg, #2f7df7 0%, #25c5ff 100%);
right: 18%;
top: 38%;
}
.pill-secondary {
width: 90px;
height: 36px;
background: linear-gradient(110deg, #36a1ff 0%, #8bd6ff 100%);
right: 32%;
top: 30%;
}
.pulse {
position: absolute;
right: 10%;
bottom: 12%;
width: 360px;
height: 140px;
background: radial-gradient(circle at 20% 50%, rgba(46, 102, 243, 0.08), transparent 40%),
radial-gradient(circle at 60% 60%, rgba(43, 180, 255, 0.12), transparent 50%);
}
.bg-wave {
position: absolute;
inset: 0;
background: radial-gradient(120% 120% at 20% 40%, rgba(255, 255, 255, 0.2), transparent 60%);
}
.bg-right {
background: radial-gradient(90% 90% at 80% 60%, rgba(255, 255, 255, 0.4), transparent 60%);
mix-blend-mode: soft-light;
}
@media (max-width: 900px) {
.login-page {
background: linear-gradient(180deg, #2e66f3 0%, #25b8ff 100%);
padding: 18px;
}
.login-card {
width: 100%;
}
.decor {
display: none;
}
}
</style>

@ -0,0 +1,840 @@
<script setup>
import { ref } from 'vue'
import LayoutFrame from '../components/LayoutFrame.vue'
const activeTab = ref('account')
const tabs = [
{ key: 'account', label: '账号管理', icon: '👤' },
{ key: 'role', label: '角色管理', icon: '🔐' },
{ key: 'log', label: '操作日志', icon: '📋' },
{ key: 'setting', label: '系统设置', icon: '⚙️' }
]
const admins = ref([
{
id: '0001',
name: '张三',
avatar: '👨',
role: '糖友圈审核员',
phone: '1949082231',
email: 'zhangsan@example.com',
created: '2024-05-01 01:09',
last: '2024-12-24 23:12',
status: 'active',
department: '内容审核部'
},
{
id: '0002',
name: '周乐心',
avatar: '👩',
role: '知识百科管理员',
phone: '19113212388',
email: 'zhoulexin@example.com',
created: '2024-02-27 14:31',
last: '2024-12-24 22:45',
status: 'active',
department: '内容运营部'
},
{
id: '0003',
name: '李天泽',
avatar: '👨',
role: '客户端管理员',
phone: '19876782134',
email: 'litianze@example.com',
created: '2024-09-14 16:22',
last: '2024-12-24 21:30',
status: 'inactive',
department: '用户运营部'
},
{
id: '0004',
name: '胡小小',
avatar: '👩',
role: '设备管理员',
phone: '1949082231',
email: 'huxiaoxiao@example.com',
created: '2024-05-01 01:09',
last: '2024-12-24 20:15',
status: 'active',
department: '设备管理部'
}
])
const roles = ref([
{ name: '超级管理员', users: 2, permissions: '全部权限', status: 'active' },
{ name: '内容审核员', users: 8, permissions: '内容审核权限', status: 'active' },
{ name: '运营管理员', users: 5, permissions: '用户运营权限', status: 'active' },
{ name: '设备管理员', users: 3, permissions: '设备管理权限', status: 'active' },
{ name: '数据分析师', users: 2, permissions: '数据查看权限', status: 'inactive' }
])
const searchQuery = ref('')
const selectedDepartment = ref('all')
const departmentOptions = [
{ value: 'all', label: '全部部门', icon: '🏢' },
{ value: 'audit', label: '内容审核部', icon: '📝' },
{ value: 'operation', label: '内容运营部', icon: '📊' },
{ value: 'user', label: '用户运营部', icon: '👥' },
{ value: 'device', label: '设备管理部', icon: '📱' }
]
const getStatusInfo = (status) => {
const statusMap = {
active: { label: '正常', color: 'green', icon: '✅' },
inactive: { label: '停用', color: 'red', icon: '❌' },
pending: { label: '待激活', color: 'orange', icon: '⏳' }
}
return statusMap[status] || statusMap.active
}
const getRoleInfo = (role) => {
const roleMap = {
'超级管理员': { color: 'red', icon: '👑' },
'内容审核员': { color: 'blue', icon: '📝' },
'运营管理员': { color: 'green', icon: '📊' },
'设备管理员': { color: 'orange', icon: '📱' },
'数据分析师': { color: 'purple', icon: '📈' }
}
return roleMap[role] || { color: 'gray', icon: '👤' }
}
const toggleUserStatus = (userId) => {
const user = admins.value.find(u => u.id === userId)
if (user) {
user.status = user.status === 'active' ? 'inactive' : 'active'
}
}
const resetPassword = (userId) => {
console.log(`重置用户 ${userId} 密码`)
}
</script>
<template>
<LayoutFrame title="系统管理">
<!-- 标签页导航 -->
<div class="app-card">
<div class="tabs-container">
<div class="tabs-header">
<h2 class="tabs-title">系统管理</h2>
<div class="tabs-actions">
<button class="btn primary" v-if="activeTab === 'account'">
<span class="btn-icon"></span>
添加账号
</button>
<button class="btn primary" v-if="activeTab === 'role'">
<span class="btn-icon"></span>
新建角色
</button>
</div>
</div>
<div class="tabs-navigation">
<button
v-for="tab in tabs"
:key="tab.key"
:class="['tab-button', { active: activeTab === tab.key }]"
@click="activeTab = tab.key"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-label">{{ tab.label }}</span>
</button>
</div>
</div>
</div>
<!-- 账号管理 -->
<div v-if="activeTab === 'account'" class="app-card">
<div class="panel-header">
<h3 class="panel-title">员工账号管理</h3>
<div class="panel-stats">
<span class="stat">总账号数: {{ admins.length }}</span>
<span class="stat">在线: {{ admins.filter(a => a.status === 'active').length }}</span>
</div>
</div>
<!-- 搜索筛选 -->
<div class="toolbar">
<div class="filter-group">
<select v-model="selectedDepartment" class="select">
<option v-for="option in departmentOptions" :key="option.value" :value="option.value">
{{ option.icon }} {{ option.label }}
</option>
</select>
</div>
<div class="search-group">
<input v-model="searchQuery" class="input" placeholder="搜索工号、姓名或手机号..." />
<button class="btn primary">搜索</button>
</div>
<div class="action-group">
<button class="btn secondary">重置</button>
<button class="btn secondary">导出名单</button>
</div>
</div>
<!-- 员工表格 -->
<div class="table-container">
<table class="table">
<thead>
<tr>
<th style="width: 80px;">工号</th>
<th style="width: 120px;">员工信息</th>
<th style="width: 120px;">联系方式</th>
<th style="width: 120px;">角色权限</th>
<th style="width: 100px;">部门</th>
<th style="width: 150px;">账号创建时间</th>
<th style="width: 150px;">最后登录时间</th>
<th style="width: 100px;">状态</th>
<th style="width: 200px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in admins" :key="item.id" class="admin-row">
<td>
<div class="admin-id">{{ item.id }}</div>
</td>
<td>
<div class="admin-info">
<div class="admin-avatar">{{ item.avatar }}</div>
<div class="admin-details">
<div class="admin-name">{{ item.name }}</div>
<div class="admin-role">{{ item.role }}</div>
</div>
</div>
</td>
<td>
<div class="contact-info">
<div class="contact-phone">{{ item.phone }}</div>
<div class="contact-email muted">{{ item.email }}</div>
</div>
</td>
<td>
<div class="role-info" :style="{
backgroundColor: `var(--color-${getRoleInfo(item.role).color}-50)`,
color: `var(--color-${getRoleInfo(item.role).color}-700)`,
borderColor: `var(--color-${getRoleInfo(item.role).color}-200)`
}">
<span class="role-icon">{{ getRoleInfo(item.role).icon }}</span>
<span class="role-name">{{ item.role }}</span>
</div>
</td>
<td>
<div class="department-info">
<span class="department-name">{{ item.department }}</span>
</div>
</td>
<td>
<div class="time-info">
<div class="create-time">{{ item.created }}</div>
</div>
</td>
<td>
<div class="time-info">
<div class="last-login">{{ item.last }}</div>
<div class="login-status" :class="item.status">
{{ item.status === 'active' ? '在线' : '离线' }}
</div>
</div>
</td>
<td>
<span class="status-tag" :class="getStatusInfo(item.status).color">
<span class="status-icon">{{ getStatusInfo(item.status).icon }}</span>
{{ getStatusInfo(item.status).label }}
</span>
</td>
<td>
<div class="action-buttons">
<button class="btn secondary small">
<span class="btn-icon">👁</span>
查看
</button>
<button @click="toggleUserStatus(item.id)" class="btn small"
:class="item.status === 'active' ? 'warning' : 'success'">
<span class="btn-icon">{{ item.status === 'active' ? '❌' : '✅' }}</span>
{{ item.status === 'active' ? '停用' : '启用' }}
</button>
<button @click="resetPassword(item.id)" class="btn secondary small">
<span class="btn-icon">🔑</span>
重置密码
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="table-footer">
<div class="pagination-info">
显示 1-4 4 条记录
</div>
<div class="pagination">
<button class="active">1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<button></button>
</div>
</div>
</div>
<!-- 角色管理 -->
<div v-if="activeTab === 'role'" class="app-card">
<div class="panel-header">
<h3 class="panel-title">角色权限管理</h3>
<div class="panel-stats">
<span class="stat">总角色数: {{ roles.length }}</span>
<span class="stat">启用: {{ roles.filter(r => r.status === 'active').length }}</span>
</div>
</div>
<div class="roles-grid">
<div v-for="role in roles" :key="role.name" class="role-card">
<div class="role-header">
<div class="role-icon" :style="{
backgroundColor: `var(--color-${getRoleInfo(role.name).color}-100)`,
color: `var(--color-${getRoleInfo(role.name).color}-600)`
}">
{{ getRoleInfo(role.name).icon }}
</div>
<div class="role-info">
<h4 class="role-name">{{ role.name }}</h4>
<div class="role-users">{{ role.users }} 个用户</div>
</div>
<span class="status-tag small" :class="getStatusInfo(role.status).color">
{{ getStatusInfo(role.status).label }}
</span>
</div>
<div class="role-permissions">
<div class="permissions-label">权限范围:</div>
<div class="permissions-text">{{ role.permissions }}</div>
</div>
<div class="role-actions">
<button class="btn secondary small">
<span class="btn-icon"></span>
编辑
</button>
<button class="btn secondary small">
<span class="btn-icon">👥</span>
分配
</button>
<button class="btn secondary small">
<span class="btn-icon">📋</span>
权限
</button>
</div>
</div>
</div>
</div>
<!-- 系统概览 -->
<div class="app-card">
<div class="panel-header">
<h3 class="panel-title">系统概览</h3>
</div>
<div class="system-overview">
<div class="overview-item">
<div class="overview-icon">👥</div>
<div class="overview-content">
<div class="overview-value">{{ admins.length }}</div>
<div class="overview-label">管理员数量</div>
</div>
</div>
<div class="overview-item">
<div class="overview-icon">🔐</div>
<div class="overview-content">
<div class="overview-value">{{ roles.length }}</div>
<div class="overview-label">角色数量</div>
</div>
</div>
<div class="overview-item">
<div class="overview-icon">📊</div>
<div class="overview-content">
<div class="overview-value">99.8%</div>
<div class="overview-label">系统可用性</div>
</div>
</div>
<div class="overview-item">
<div class="overview-icon">🛡</div>
<div class="overview-content">
<div class="overview-value">0</div>
<div class="overview-label">安全事件</div>
</div>
</div>
</div>
</div>
</LayoutFrame>
</template>
<style scoped>
.tabs-container {
padding: 0;
}
.tabs-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-6) var(--space-6) 0;
margin-bottom: var(--space-4);
}
.tabs-title {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-gray-900);
margin: 0;
}
.tabs-navigation {
display: flex;
border-bottom: 1px solid var(--color-gray-200);
padding: 0 var(--space-6);
}
.tab-button {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: transparent;
border: none;
color: var(--color-gray-600);
font-weight: 600;
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
}
.tab-button:hover {
color: var(--color-primary-600);
}
.tab-button.active {
color: var(--color-primary-600);
border-bottom-color: var(--color-primary-600);
}
.tab-icon {
font-size: var(--text-base);
}
.admin-row {
transition: all 0.3s ease;
}
.admin-row:hover {
background: var(--color-primary-50) !important;
}
.admin-id {
font-family: 'Monaco', 'Consolas', monospace;
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.admin-info {
display: flex;
align-items: center;
gap: var(--space-3);
}
.admin-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--color-gray-200);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
flex-shrink: 0;
}
.admin-details {
flex: 1;
min-width: 0;
}
.admin-name {
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--space-1);
font-size: var(--text-sm);
}
.admin-role {
font-size: var(--text-xs);
color: var(--color-gray-500);
}
.contact-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.contact-phone {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.contact-email {
font-size: var(--text-xs);
}
.role-info {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.role-icon {
font-size: var(--text-xs);
}
.department-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.department-name {
font-weight: 500;
color: var(--color-gray-700);
font-size: var(--text-sm);
}
.time-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.create-time,
.last-login {
font-weight: 600;
color: var(--color-gray-900);
font-size: var(--text-sm);
}
.login-status {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
font-weight: 500;
text-align: center;
}
.login-status.active {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-600);
}
.login-status.inactive {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-600);
}
.status-tag {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: 600;
border: 1px solid;
}
.status-tag.green {
background: rgba(16, 185, 129, 0.1);
color: var(--color-success-700);
border-color: rgba(16, 185, 129, 0.2);
}
.status-tag.red {
background: rgba(239, 68, 68, 0.1);
color: var(--color-error-700);
border-color: rgba(239, 68, 68, 0.2);
}
.status-tag.orange {
background: rgba(245, 158, 11, 0.1);
color: var(--color-warning-700);
border-color: rgba(245, 158, 11, 0.2);
}
.status-icon {
font-size: var(--text-xs);
}
.status-tag.small {
padding: var(--space-1) var(--space-2);
font-size: var(--text-xs);
}
.action-buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.btn.warning {
background: linear-gradient(135deg, var(--color-warning-500) 0%, var(--color-warning-600) 100%);
color: #fff;
box-shadow: var(--shadow-md);
}
.btn.warning:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.roles-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--space-5);
padding: var(--space-5);
}
.role-card {
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
transition: all 0.3s ease;
}
.role-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary-300);
}
.role-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.role-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
flex-shrink: 0;
}
.role-info {
flex: 1;
}
.role-name {
font-weight: 600;
color: var(--color-gray-900);
margin: 0 0 var(--space-1);
font-size: var(--text-base);
}
.role-users {
font-size: var(--text-sm);
color: var(--color-gray-500);
}
.role-permissions {
margin-bottom: var(--space-3);
}
.permissions-label {
font-weight: 600;
color: var(--color-gray-700);
font-size: var(--text-sm);
margin-bottom: var(--space-1);
}
.permissions-text {
font-size: var(--text-sm);
color: var(--color-gray-600);
line-height: var(--line-height-relaxed);
}
.role-actions {
display: flex;
gap: var(--space-2);
}
.system-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-5);
padding: var(--space-5);
}
.overview-item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-4);
background: var(--color-gray-50);
border-radius: var(--radius-xl);
border: 1px solid var(--color-gray-200);
transition: all 0.3s ease;
}
.overview-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary-300);
}
.overview-icon {
font-size: 2rem;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
.overview-content {
flex: 1;
}
.overview-value {
font-size: var(--text-2xl);
font-weight: 800;
color: var(--color-gray-900);
line-height: 1;
margin-bottom: var(--space-1);
}
.overview-label {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
}
.panel-stats {
display: flex;
gap: var(--space-4);
}
.stat {
font-size: var(--text-sm);
color: var(--color-gray-600);
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
gap: var(--space-4);
}
.filter-group,
.search-group,
.action-group {
width: 100%;
}
.table-container {
overflow-x: auto;
}
.roles-grid {
grid-template-columns: 1fr;
}
.tabs-header {
flex-direction: column;
gap: var(--space-3);
align-items: stretch;
}
}
@media (max-width: 768px) {
.action-buttons {
flex-direction: column;
}
.action-buttons .btn {
width: 100%;
justify-content: center;
}
.admin-info {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
.tabs-navigation {
flex-wrap: wrap;
}
.tab-button {
flex: 1;
min-width: 120px;
justify-content: center;
}
.system-overview {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.system-overview {
grid-template-columns: 1fr;
}
.overview-item {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
.role-header {
flex-direction: column;
text-align: center;
gap: var(--space-2);
}
.role-actions {
flex-direction: column;
}
.role-actions .btn {
width: 100%;
justify-content: center;
}
}
</style>

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})

@ -1,7 +1,9 @@
{
"pages": [
"pages/login/login",
"pages/index/index",
"pages/family/family",
"pages/family/invite/invite",
"pages/circle/circle",
"pages/circle/detail/detail",
"pages/circle/report/report",
@ -10,16 +12,17 @@
"pages/monitor/adjust-plan/adjust-plan",
"pages/history/history",
"pages/history/manual-entry/manual-entry",
"pages/history/family-members",
"pages/history/member-history",
"pages/knowledge/knowledge",
"pages/knowledge/detail/detail",
"pages/chat/chat",
"pages/message/message",
"pages/history/manual-entry"
"pages/message/message"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#2196F3",
"navigationBarTitleText": "智糖管理",
"navigationBarTitleText": "GlucoWell",
"navigationBarTextStyle": "white",
"backgroundColor": "#f8fbff",
"enablePullDownRefresh": true

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762867735180" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3180" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M546 1024c-30.3 0-59.7-15.7-75.9-43.7-7.7-13.3-11.7-28.4-11.7-43.7V583.3L155.3 759.5c-19.6 11.4-44.5 4.7-55.9-14.8s-4.7-44.5 14.8-55.9l301.3-175.1-301.1-172.6c-19.6-11.2-26.3-36.2-15.1-55.8s36.2-26.4 55.8-15.1l303.4 173.9V87.4c0-48.2 39.2-87.4 87.4-87.4 15.3 0 30.5 4.1 43.7 11.7l296.8 171.4c20.2 11.7 34.7 30.5 40.7 53.1s2.9 46.1-8.7 66.4c-7.7 13.3-18.7 24.3-32 32L579 513.2l307.3 176.2c20.3 11.7 34.8 30.6 40.9 53.2 6 22.6 2.9 46.1-8.7 66.4-7.7 13.3-18.7 24.3-32 32l-296.9 171.4c-13.8 7.8-28.8 11.6-43.6 11.6z m-5.8-438.8v351.4c0 1 0.3 2 0.8 2.8 1.6 2.7 5.1 3.6 7.8 2.1l296.9-171.4c0.9-0.5 1.6-1.2 2.1-2.1 1-1.7 0.8-3.3 0.6-4.3-0.3-1-1-2.5-2.7-3.4L540.2 585.2z m5.6-503.5c-3.1 0-5.7 2.6-5.7 5.7v353.8l305.3-177.4c1-0.6 1.7-1.3 2.2-2.2 1-1.7 0.8-3.3 0.6-4.3-0.3-1-1-2.5-2.7-3.5L548.7 82.5c-0.9-0.5-1.9-0.8-2.9-0.8z" fill="#707070" p-id="3181"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762915282351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10319" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m213.333333 554.666667h-170.666666v170.666666a42.666667 42.666667 0 0 1-85.333334 0v-170.666666H298.666667a42.666667 42.666667 0 0 1 0-85.333334h170.666666V298.666667a42.666667 42.666667 0 0 1 85.333334 0v170.666666h170.666666a42.666667 42.666667 0 0 1 0 85.333334z" fill="#3E2AD1" p-id="10320"></path></svg>

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@ -16,12 +16,45 @@ Page({
allChatHistory: [],
filteredHistory: [],
showFunctionMenu: false,
pageLoading: true,
},
// 新增的方法
getCurrentTime: function () {
const now = new Date();
const hours = now.getHours().toString().padStart(2, "0");
const minutes = now.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
},
getCurrentDate: function () {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, "0");
const day = now.getDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
},
scrollToBottom: function () {
setTimeout(() => {
this.setData({
scrollToView: "scroll-bottom",
});
}, 100);
},
onLoad: function () {
this.setData({ pageLoading: true });
this.loadUserInfo();
this.initChat();
this.loadAllChatHistory();
// 页面加载完成
setTimeout(() => {
this.setData({ pageLoading: false });
this.scrollToBottom();
}, 300);
},
loadUserInfo: function () {
@ -30,13 +63,14 @@ Page({
},
initChat: function () {
// 初始化聊天,可以加载历史记录
const chatHistory = wx.getStorageSync("chatHistory") || [];
this.setData({ messages: chatHistory });
// 如果是新用户,显示欢迎消息
// 如果是新用户,添加欢迎消息
if (chatHistory.length === 0) {
this.addWelcomeMessage();
} else {
this.setData({ messages: chatHistory });
this.scrollToBottom();
}
},
@ -45,15 +79,15 @@ Page({
id: Date.now(),
role: "assistant",
content:
"你好!我是糖小智,你的智能健康助手。\n\n我可以为你\n• 回答健康相关问题\n• 提供饮食建议\n• 制定运动计划\n• 解答血糖监测疑问\n\n请选择上方的问题开始对话或直接输入你的问题",
"嗨!我是糖小智 👋\n\n我可以为你\n🍎 制定健康饮食计划\n🏃 推荐运动方案\n🩺 解答血糖监测疑问\n📚 提供最新健康资讯\n\n📸 还支持:\n• 拍照识别血糖数据\n• 图片识别饮食信息\n• 搜索历史对话\n\n💡 点击下方问题开始对话,或点击+号使用更多功能",
avatar: "/images/avatars/assistant.png",
time: this.getCurrentTime(),
isWelcome: true,
};
const messages = [welcomeMessage];
this.setData({ messages });
wx.setStorageSync("chatHistory", messages);
this.saveToAllHistory(welcomeMessage);
},
loadAllChatHistory: function () {
@ -345,7 +379,7 @@ Page({
// 保留原来的简单回复作为备用
const responses = {
含糖量:
"含糖量低的食物包括:绿叶蔬菜、黄瓜、西红、西兰花、蘑菇、苹果、草莓、蓝莓等。建议选择高纤维、低GI值的食物。",
"含糖量低的食物包括:绿叶蔬菜、黄瓜、西红 tomatoes、西兰花、蘑菇、苹果、草莓、蓝莓等。建议选择高纤维、低GI值的食物。",
无糖可乐:
"无糖可乐使用人工甜味剂代替糖,热量较低。但建议适量饮用,因为人工甜味剂可能影响肠道菌群和胰岛素敏感性。",
测血糖:
@ -463,50 +497,145 @@ Page({
this.setData({ showFunctionMenu: false });
},
// 选择图片功能
// 修改 chooseFile 方法
chooseFile: function () {
this.closeFunctionMenu();
wx.showActionSheet({
itemList: ["选择文档", "选择图片", "选择视频"],
success: (res) => {
const tapIndex = res.tapIndex;
switch (tapIndex) {
case 0: // 文档
this.chooseDocument();
break;
case 1: // 图片
this.chooseImage({ currentTarget: { dataset: { type: "album" } } });
break;
case 2: // 视频
this.chooseVideo();
break;
}
},
fail: (err) => {
console.error("选择文件失败:", err);
},
});
},
// 新增:选择文档方法
chooseDocument: function () {
wx.chooseMessageFile({
count: 1,
type: "file",
success: (res) => {
const file = res.tempFiles[0];
this.handleFileUpload(file);
},
fail: (err) => {
console.error("选择文档失败:", err);
wx.showToast({
title: "选择文档失败",
icon: "none",
});
},
});
},
// 新增:选择视频方法
chooseVideo: function () {
wx.chooseVideo({
sourceType: ["album", "camera"],
maxDuration: 60,
camera: "back",
success: (res) => {
const videoFile = {
path: res.tempFilePath,
size: res.size,
type: "video",
duration: res.duration,
name: "video_" + Date.now() + ".mp4",
};
this.handleFileUpload(videoFile);
},
fail: (err) => {
console.error("选择视频失败:", err);
wx.showToast({
title: "选择视频失败",
icon: "none",
});
},
});
},
// 修改:增强的图片选择方法
chooseImage: function (e) {
const type = e.currentTarget.dataset.type;
const sourceType = type === "camera" ? ["camera"] : ["album"];
this.closeFunctionMenu();
// 如果不是由 chooseFile 调用,则关闭菜单
if (e.currentTarget.dataset.from !== "actionsheet") {
this.closeFunctionMenu();
}
wx.chooseImage({
count: 1,
sizeType: ["compressed"],
sizeType: ["original", "compressed"],
sourceType: sourceType,
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
this.handleImageUpload(tempFilePath, type);
const fileInfo = {
path: tempFilePath,
size: res.tempFiles[0].size,
type: "image",
name: "image_" + Date.now() + ".jpg",
};
if (type === "camera") {
this.handleImageUpload(tempFilePath, "拍照");
} else {
// 对于相册选择的图片,提供两种处理方式
wx.showActionSheet({
itemList: ["识别图片文字", "作为文件上传"],
success: (actionRes) => {
if (actionRes.tapIndex === 0) {
this.handleImageUpload(tempFilePath, "图片");
} else {
this.handleFileUpload(fileInfo);
}
},
});
}
},
fail: (err) => {
console.error("选择图片失败:", err);
wx.showToast({
title: type === "camera" ? "拍照失败" : "选择图片失败",
icon: "none",
});
if (err.errMsg !== "chooseImage:fail cancel") {
wx.showToast({
title: type === "camera" ? "拍照失败" : "选择图片失败",
icon: "none",
});
}
},
});
},
// 处理图片上传
handleImageUpload: function (imagePath, type) {
const typeText = type === "camera" ? "拍照" : "图片";
// 新增:处理文件上传
handleFileUpload: function (file) {
// 显示文件信息
this.addFileMessage("user", file);
// 添加用户消息(显示图片)
this.addImageMessage("user", imagePath, `${typeText}识别文字`);
// 模拟OCR识别
this.performOCR(imagePath, typeText);
// 模拟文件上传过程
this.simulateFileUpload(file);
},
// 添加图片消息
addImageMessage: function (role, imagePath, description) {
// 新增:添加文件消息
addFileMessage: function (role, file) {
const message = {
id: Date.now(),
role: role,
type: "image",
imagePath: imagePath,
content: description,
type: "file",
fileInfo: file,
content: this.getFileDescription(file),
avatar:
role === "user"
? this.data.userInfo.avatar || "/images/avatars/default-avatar.png"
@ -519,142 +648,264 @@ Page({
wx.setStorageSync("chatHistory", messages);
this.saveToAllHistory(message);
this.scrollToBottom();
return message.id;
},
// 模拟OCR识别
performOCR: function (imagePath, typeText) {
this.setData({ isLoading: true });
// 新增:获取文件描述
getFileDescription: function (file) {
const fileSize = this.formatFileSize(file.size);
switch (file.type) {
case "image":
return `图片文件 (${fileSize})`;
case "video":
return `视频文件 (${fileSize}, ${Math.round(file.duration)}秒)`;
case "file":
const ext = file.name.split(".").pop().toLowerCase();
return `${this.getFileTypeName(ext)}文档 (${fileSize})`;
default:
return `文件 (${fileSize})`;
}
},
const thinkingId = "ocr-thinking-" + Date.now();
// 显示识别中状态
const thinkingMessage = {
id: thinkingId,
// 新增:格式化文件大小
formatFileSize: function (bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
},
// 新增:获取文件类型名称
getFileTypeName: function (ext) {
const typeMap = {
pdf: "PDF",
doc: "Word",
docx: "Word",
xls: "Excel",
xlsx: "Excel",
ppt: "PowerPoint",
pptx: "PowerPoint",
txt: "文本",
zip: "压缩",
rar: "压缩",
};
return typeMap[ext] || ext.toUpperCase();
},
// 新增:模拟文件上传过程
simulateFileUpload: function (file) {
const uploadId = "file-upload-" + Date.now();
// 显示上传中状态
const uploadMessage = {
id: uploadId,
role: "assistant",
content: `正在识别${typeText}中的文字...`,
content: `正在上传${this.getFileDescription(file)}...`,
avatar: "/images/avatars/assistant.png",
time: this.getCurrentTime(),
isThinking: true,
isUploading: true,
progress: 0,
};
this.setData({
messages: [...this.data.messages, thinkingMessage],
messages: [...this.data.messages, uploadMessage],
});
this.scrollToBottom();
// 模拟OCR处理时间
setTimeout(() => {
const ocrResult = this.generateOCRResult(typeText);
// 模拟上传进度
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 20;
if (progress >= 100) {
progress = 100;
clearInterval(progressInterval);
// 上传完成,处理文件
setTimeout(() => {
this.processUploadedFile(file, uploadId);
}, 500);
}
// 移除思考状态,添加识别结果
const messages = this.data.messages;
const thinkingIndex = messages.findIndex((msg) => msg.id === thinkingId);
// 更新进度
const messages = this.data.messages.map((msg) => {
if (msg.id === uploadId) {
return {
...msg,
progress: Math.min(progress, 100),
};
}
return msg;
});
this.setData({ messages });
}, 200);
},
if (thinkingIndex !== -1) {
messages[thinkingIndex] = {
// 新增:处理上传后的文件
processUploadedFile: function (file, uploadId) {
const analysisResult = this.analyzeFileContent(file);
// 移除上传状态,添加分析结果
const messages = this.data.messages.map((msg) => {
if (msg.id === uploadId) {
return {
id: Date.now(),
role: "assistant",
content: ocrResult,
content: analysisResult,
avatar: "/images/avatars/assistant.png",
time: this.getCurrentTime(),
};
this.setData({
messages: messages,
isLoading: false,
});
wx.setStorageSync("chatHistory", messages);
this.saveToAllHistory(messages[thinkingIndex]);
} else {
this.addMessage("assistant", ocrResult);
this.setData({ isLoading: false });
}
this.scrollToBottom();
}, 2000 + Math.random() * 1000);
},
// 生成OCR结果
generateOCRResult: function (typeText) {
const results = [
`📋 ${typeText}识别完成!\n\n识别到的文字内容:\n"血糖值7.2 mmol/L\n检测时间2025-11-10 14:30"\n\n🩺 分析建议:\n• 餐后血糖略高,建议控制饮食\n• 增加适量运动\n• 继续监测血糖变化\n\n💡 如需详细分析,请咨询医生或使用应用内的血糖管理功能。`,
return msg;
});
`📸 ${typeText}文字识别结果:\n\n识别内容:\n"低糖饮食食谱\n早餐:燕麦粥+鸡蛋\n午餐:蒸蛋羹+青菜\n晚餐:瘦肉汤+豆腐"\n\n🍎 营养建议:\n• 这是一个不错的低糖饮食搭配\n• 建议增加优质蛋白质\n• 可以适当添加坚果类食物\n\n需要更详细的饮食建议吗?`,
this.setData({ messages });
wx.setStorageSync("chatHistory", messages);
`🔍 图片文字提取成功:\n\n提取的文字:\n"运动计划\n周一三五快走30分钟\n周二四力量训练20分钟\n周末:游泳或瑜伽"\n\n💪 运动评估:\n• 运动频率安排合理\n• 有氧和无氧运动结合很好\n• 建议餐后1-2小时运动\n\n要我为你制定更个性化的运动方案吗?`,
];
// 保存到历史记录
const finalMessage = messages.find((msg) => msg.content === analysisResult);
if (finalMessage) {
this.saveToAllHistory(finalMessage);
}
return results[Math.floor(Math.random() * results.length)];
this.scrollToBottom();
},
// 选择文件功能
chooseFile: function () {
this.closeFunctionMenu();
wx.showToast({
title: "文件上传功能开发中",
icon: "none",
duration: 2000,
});
// 新增:分析文件内容
analyzeFileContent: function (file) {
const fileSize = this.formatFileSize(file.size);
// 这里可以实现文件选择逻辑
// 由于小程序API限制文件上传需要后端支持
/*
wx.chooseMessageFile({
count: 1,
type: 'file',
success: (res) => {
// 处理文件上传
console.log('选择的文件:', res.tempFiles);
}
});
*/
// 根据文件类型生成不同的分析结果
switch (file.type) {
case "image":
return this.generateImageAnalysis(file);
case "video":
return this.generateVideoAnalysis(file);
default:
return this.generateDocumentAnalysis(file);
}
},
getCurrentTime: function () {
const now = new Date();
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
// 新增:生成图片分析结果
generateImageAnalysis: function (file) {
return `📷 图片分析完成!\n\n📊 文件信息:\n• 大小:${this.formatFileSize(
file.size
)}\n 类型图片文件\n\n🩺 健康数据分析\n 检测到可能的血糖数据图表\n 建议结合具体数值进行专业分析\n 如需详细解读请咨询医疗专业人员\n\n💡 提示您可以描述图片内容我会提供相关健康建议`;
},
getCurrentDate: function () {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
// 新增:生成视频分析结果
generateVideoAnalysis: function (file) {
return `🎥 视频分析完成!\n\n📊 文件信息:\n• 大小:${this.formatFileSize(
file.size
)}\n 时长${Math.round(
file.duration
)}\n 类型视频文件\n\n🏃 运动分析\n 视频可用于运动姿势评估\n 建议记录运动时长和强度\n 注意运动安全避免受伤\n\n💡 提示请描述视频内容如运动类型时长等我会提供专业建议`;
},
scrollToBottom: function () {
// 延迟执行确保DOM更新完成
setTimeout(() => {
this.setData({
scrollToView: "scroll-bottom",
});
}, 100);
// 新增:生成文档分析结果
generateDocumentAnalysis: function (file) {
const ext = file.name.split(".").pop().toLowerCase();
const typeName = this.getFileTypeName(ext);
const analyses = [
`📄 ${typeName}文档分析完成!\n\n📊 文件信息:\n• 大小:${this.formatFileSize(
file.size
)}\n 类型${typeName}文档\n\n🩺 健康报告解读\n 检测到医疗相关数据\n 建议重点关注血糖血压等关键指标\n 定期跟踪数据变化趋势\n\n💡 如需具体分析请描述文档内容或关键数据`,
`📋 ${typeName}文件已接收!\n\n📈 数据分析建议:\n• 记录血糖监测数据\n• 跟踪饮食摄入情况\n• 分析运动效果\n• 定期评估健康状况\n\n🎯 专业提示:保持规律记录,及时发现异常变化。`,
`🔍 ${typeName}文档处理完成!\n\n💊 健康管理建议:\n• 建立个人健康档案\n• 制定个性化管理计划\n• 定期复查评估\n• 调整治疗方案\n\n📝 提示:请提供具体数据或问题,我会给出针对性建议。`,
];
return analyses[Math.floor(Math.random() * analyses.length)];
},
onShareAppMessage: function () {
return {
title: "糖小智 - 您的智能健康助手",
path: "/pages/chat/chat",
// 处理图片上传
handleImageUpload: function (imagePath, type) {
// 显示图片消息
this.addMessage("user", imagePath, "image");
// 模拟图片上传过程
const uploadId = "image-upload-" + Date.now();
const uploadMessage = {
id: uploadId,
role: "assistant",
content: "正在上传图片...",
avatar: "/images/avatars/assistant.png",
time: this.getCurrentTime(),
isUploading: true,
progress: 0,
};
},
// 添加搜索高亮方法
highlightSearchText: function (keyword, text) {
if (!keyword || !text) return text;
this.setData({
messages: [...this.data.messages, uploadMessage],
});
this.scrollToBottom();
// 模拟上传进度
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 20;
if (progress >= 100) {
progress = 100;
clearInterval(progressInterval);
// 上传完成,进行图片分析
setTimeout(() => {
this.processImageUpload(imagePath, uploadId);
}, 500);
}
const regex = new RegExp(`(${this.escapeRegExp(keyword)})`, "gi");
return text.replace(regex, '<mark class="search-highlight">$1</mark>');
},
// 更新进度
const messages = this.data.messages.map((msg) => {
if (msg.id === uploadId) {
return {
...msg,
progress: Math.min(progress, 100),
};
}
return msg;
});
// 转义正则特殊字符
escapeRegExp: function (string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
this.setData({ messages });
}, 200);
},
// 新增:阻止事件冒泡的方法
stopPropagation: function (e) {
// 空方法,仅用于阻止事件冒泡
// 处理图片上传后的逻辑
processImageUpload: function (imagePath, uploadId) {
// 这里可以添加对上传图片的处理逻辑,比如调用云函数进行图片识别等
const analysisResult = `🖼️ 图片分析结果:\n\n• 图片大小:${this.formatFileSize(
imagePath.size
)}\n 上传时间${this.getCurrentTime()}\n\n如需详细分析请使用专业工具或咨询医生`;
// 移除上传状态,添加分析结果
const messages = this.data.messages.map((msg) => {
if (msg.id === uploadId) {
return {
id: Date.now(),
role: "assistant",
content: analysisResult,
avatar: "/images/avatars/assistant.png",
time: this.getCurrentTime(),
};
}
return msg;
});
this.setData({ messages });
wx.setStorageSync("chatHistory", messages);
// 保存到历史记录
const finalMessage = messages.find(
(msg) => msg.id !== uploadId && msg.content === analysisResult
);
if (finalMessage) {
this.saveToAllHistory(finalMessage);
}
this.scrollToBottom();
},
});

@ -1,5 +1,12 @@
<view class="container" bindtap="closeFunctionMenu">
<view class="chat-wrapper">
<view class="container">
<!-- 页面加载遮罩 -->
<view class="page-loading" wx:if="{{pageLoading}}">
<view class="loading-content">
<text>加载中...</text>
</view>
</view>
<view class="chat-wrapper" wx:if="{{!pageLoading}}">
<!-- 猜你想问 -->
<view class="suggested-questions card" wx:if="{{messages.length <= 1}}">
<text class="section-title">💡 猜你想问</text>
@ -11,37 +18,65 @@
</view>
</view>
<!-- 聊天记录 -->
<scroll-view class="chat-messages" scroll-y="true" scroll-into-view="{{scrollToView}}" enhanced="{{true}}">
<view class="messages-container">
<view class="message-item {{item.role}} {{item.isThinking ? 'thinking' : ''}}"
wx:for="{{messages}}" wx:key="id" id="message-{{item.id}}">
<image src="{{item.avatar}}" class="message-avatar"></image>
<view class="message-content">
<!-- 图片消息 -->
<view class="image-message" wx:if="{{item.type === 'image'}}">
<image src="{{item.imagePath}}" class="message-image" mode="aspectFit"></image>
<text class="image-description">{{item.content}}</text>
</view>
<!-- 普通文字消息 -->
<text class="message-text" wx:elif="{{!item.isThinking}}">{{item.content}}</text>
<!-- 思考动画 -->
<view class="thinking-animation" wx:if="{{item.isThinking}}">
<text class="thinking-text">{{item.content}}</text>
<view class="thinking-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
<!-- 聊天记录区域 - 修复滑动问题 -->
<view class="chat-messages-container">
<scroll-view class="chat-messages" scroll-y="true" scroll-into-view="{{scrollToView}}" enhanced="{{true}}" show-scrollbar="{{false}}">
<view class="messages-container">
<!-- 消息列表 -->
<view class="message-item {{item.role}} {{item.isThinking ? 'thinking' : ''}}"
wx:for="{{messages}}" wx:key="id" id="message-{{item.id}}">
<image src="{{item.avatar}}" class="message-avatar"></image>
<view class="message-content">
<!-- 图片消息 -->
<view class="image-message" wx:if="{{item.type === 'image'}}">
<image src="{{item.imagePath}}" class="message-image" mode="aspectFit"></image>
<text class="image-description">{{item.content}}</text>
</view>
<!-- 普通文字消息 -->
<text class="message-text" wx:elif="{{!item.isThinking}}">{{item.content}}</text>
<!-- 文件消息 -->
<view class="file-message" wx:elif="{{item.type === 'file'}}">
<view class="file-item">
<image src="/images/icons/file.png" class="file-icon"></image>
<view class="file-info">
<text class="file-name">{{item.fileInfo.name}}</text>
<text class="file-meta">{{item.content}}</text>
</view>
</view>
<!-- 上传进度 -->
<view class="upload-progress" wx:if="{{item.isUploading}}">
<view class="progress-bar">
<view class="progress-fill" style="width: {{item.progress}}%"></view>
</view>
<text class="progress-text">上传中... {{item.progress}}%</text>
</view>
<!-- 视频预览 -->
<view class="video-preview" wx:if="{{item.fileInfo.type === 'video'}}">
<video src="{{item.fileInfo.path}}" controls class="video-thumbnail">
<view class="video-play-icon">▶</view>
</video>
</view>
</view>
<!-- 思考动画 -->
<view class="thinking-animation" wx:if="{{item.isThinking}}">
<text class="thinking-text">{{item.content}}</text>
<view class="thinking-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
<text class="message-time">{{item.time}}</text>
</view>
<text class="message-time">{{item.time}}</text>
</view>
<!-- 底部占位,确保滚动到底 -->
<view class="scroll-placeholder" id="scroll-bottom"></view>
</view>
<!-- 底部占位,确保滚动到底 -->
<view class="scroll-placeholder" id="scroll-bottom"></view>
</view>
</scroll-view>
</scroll-view>
</view>
</view>
<!-- 历史记录悬浮按钮 -->
@ -49,89 +84,84 @@
<image src="/images/icons/history.svg" class="history-icon"></image>
</view>
<!-- 输入 -->
<view class="input-container" catchtap="">
<!-- 输入区域 -->
<view class="input-section" catchtap="closeFunctionMenu">
<!-- 功能选项菜单 -->
<view class="function-menu" wx:if="{{showFunctionMenu}}">
<view class="function-item" bindtap="chooseImage" data-type="camera">
<image src="/images/icons/camera.png" class="function-icon"></image>
<text class="function-text">拍照识别文字</text>
</view>
<view class="function-item" bindtap="chooseImage" data-type="album">
<image src="/images/icons/image.png" class="function-icon"></image>
<text class="function-text">图片识别文字</text>
</view>
<view class="function-item" bindtap="chooseFile">
<image src="/images/icons/file.png" class="function-icon"></image>
<text class="function-text">文件</text>
<text class="function-text">上传文件</text>
</view>
</view>
<view class="input-container">
<view class="input-box">
<view class="add-btn" catchtap="toggleFunctionMenu">
<image src="/images/icons/add.png" class="add-icon {{showFunctionMenu ? 'rotated' : ''}}"></image>
</view>
<input type="text" placeholder="请输入问题或选择功能..." value="{{inputText}}"
bindinput="onInput" bindconfirm="sendMessage"
class="message-input" disabled="{{isLoading}}" maxlength="500"/>
<view class="send-btn {{inputText.trim() || isLoading ? 'active' : ''}}" bindtap="sendMessage">
<view class="send-icon"></view>
</view>
<view class="input-box">
<view class="add-btn" catchtap="toggleFunctionMenu">
<image src="/images/icons/add.png" class="add-icon {{showFunctionMenu ? 'rotated' : ''}}"></image>
</view>
<view class="input-status" wx:if="{{isLoading}}">
<text class="status-text">🤔 糖小智正在思考...</text>
<textarea placeholder="请输入问题或选择功能..." value="{{inputText}}"
bindinput="onInput" bindconfirm="sendMessage"
class="message-input" disabled="{{isLoading}}" maxlength="500"
auto-height="true"
fixed="false"
show-confirm-bar="false"
cursor-spacing="10"
placeholder-style="color: #999; line-height: 60rpx;"
></textarea>
<!-- 简化的发送按钮 - 只保留图标 -->
<view class="send-btn {{inputText.trim() || isLoading ? 'active' : ''}}" bindtap="sendMessage">
<image src="/images/icons/send-message.png" class="send-icon-image"></image>
</view>
</view>
<view class="input-status" wx:if="{{isLoading}}">
<text class="status-text">🤔 糖小智正在思考...</text>
</view>
</view>
<!-- 历史对话搜索弹窗 -->
<view class="history-overlay" wx:if="{{showHistorySearch}}" bindtap="hideHistorySearch">
<view class="history-modal" catchtap="stopPropagation">
<view class="history-overlay" wx:if="{{showHistorySearch}}" bindtap="hideHistorySearch">
<view class="history-modal" catchtap="stopPropagation">
<!-- 弹窗头部 -->
<view class="modal-header">
<text class="modal-title">📚 历史对话</text>
<view class="modal-title-container">
<text class="modal-title-icon">📚</text>
<text class="modal-title-text">历史记录</text>
</view>
<view class="close-btn" bindtap="hideHistorySearch">✕</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input type="text" placeholder="搜索对话内容..."
value="{{searchKeyword}}" bindinput="onSearchInput"
class="search-input"/>
<image src="/images/icons/search.png" class="search-icon"></image>
</view>
<!-- 历史记录列表 -->
<scroll-view class="history-list" scroll-y="true">
<!-- 搜索无结果 -->
<view class="history-empty" wx:if="{{searchKeyword && filteredHistory.length === 0}}">
<text class="empty-text">🔍 没有找到相关对话</text>
</view> <!-- 搜索框 -->
<view class="search-box">
<input type="text" placeholder="搜索对话内容..."
value="{{searchKeyword}}" bindinput="onSearchInput"
class="search-input"/>
<image src="/images/icons/search.png" class="search-icon"></image>
</view>
<!-- 历史记录项 -->
<view class="history-item" wx:for="{{searchKeyword ? filteredHistory : allChatHistory}}"
wx:key="id" bindtap="loadHistorySession" data-session-id="{{item.id}}">
<view class="history-info">
<text class="history-title">{{item.title}}</text>
<text class="history-date">{{item.date}}</text>
<!-- 历史记录列表 -->
<scroll-view class="history-list" scroll-y="true">
<!-- 搜索无结果 -->
<view class="history-empty" wx:if="{{searchKeyword && filteredHistory.length === 0}}">
<text class="empty-text">🔍 没有找到相关对话</text>
</view>
<text class="history-preview">{{item.messages[0].content}}</text>
</view>
<!-- 无历史记录 -->
<view class="history-empty" wx:if="{{allChatHistory.length === 0 && !searchKeyword}}">
<text class="empty-text">📝 暂无历史对话记录</text>
</view>
</scroll-view>
</view>
</view>
<!-- 欢迎语 -->
<view class="welcome-message card" wx:if="{{messages.length === 0}}">
<text class="welcome-text">嗨!我是糖小智 👋\n\n我可以为你\n🍎 制定健康饮食计划\n🏃 推荐运动方案\n🩺 解答血糖监测疑问\n📚 提供最新健康资讯\n\n📸 还支持:\n• 拍照识别血糖数据\n• 图片识别饮食信息\n• 搜索历史对话</text>
<view class="welcome-actions">
<text class="welcome-tip">💡 点击下方问题开始对话,或点击+号使用更多功能</text>
<!-- 历史记录项 -->
<view class="history-item" wx:for="{{searchKeyword ? filteredHistory : allChatHistory}}"
wx:key="id" bindtap="loadHistorySession" data-session-id="{{item.id}}">
<view class="history-info">
<text class="history-title">{{item.title}}</text>
<text class="history-date">{{item.date}}</text>
</view>
<text class="history-preview">{{item.messages[0].content}}</text>
</view>
<!-- 无历史记录 -->
<view class="history-empty" wx:if="{{allChatHistory.length === 0 && !searchKeyword}}">
<text class="empty-text">📝 暂无历史对话记录</text>
</view>
</scroll-view>
</view>
</view>
</view>

@ -18,13 +18,15 @@
display: flex;
flex-direction: column;
background: #f8f9fa;
position: relative;
}
.chat-wrapper {
flex: 1;
overflow-y: auto;
padding: 30rpx 20rpx;
padding-bottom: 200rpx; /* 增加底部间距 */
display: flex;
flex-direction: column;
overflow: hidden;
padding: 30rpx 20rpx 0;
}
.chat-header {
@ -66,35 +68,38 @@
opacity: 0.8;
}
/* 历史记录悬浮按钮 */
/* 历史记录悬浮按钮 - 调整到右上角 */
.history-fab {
position: fixed;
right: 30rpx;
bottom: 220rpx; /* 调整到输入框上方 */
width: 100rpx;
height: 100rpx;
top: 100rpx; /* 向上移动 20rpx */
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(
135deg,
var(--primary-medical) 0%,
var(--primary-dark) 100%
);
#2196f3 0%,
#1976d2 100%
); /* 蓝色渐变背景 */
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 25rpx var(--shadow-light);
box-shadow: 0 6rpx 20rpx rgba(33, 150, 243, 0.3); /* 蓝色阴影 */
z-index: 999;
transition: all 0.3s ease;
border: 2rpx solid rgba(255, 255, 255, 0.3); /* 浅色边框 */
}
.history-fab:active {
transform: scale(0.9);
box-shadow: 0 4rpx 15rpx rgba(56, 142, 60, 0.5);
box-shadow: 0 3rpx 12rpx rgba(25, 118, 210, 0.5);
}
.history-icon {
width: 50rpx;
height: 50rpx;
width: 40rpx;
height: 40rpx;
/* 白色图标,在蓝色背景上清晰可见 */
filter: brightness(0) invert(1);
}
.header-actions {
@ -184,8 +189,19 @@
box-shadow: 0 2rpx 12rpx var(--shadow-light);
}
/* 聊天消息容器 */
.chat-messages-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
margin-bottom: 20rpx;
height: calc(100vh - 400rpx); /* 确保有固定高度 */
}
.chat-messages {
flex: 1;
height: 100%;
background: transparent;
padding: 0;
margin: 0;
@ -193,6 +209,7 @@
.messages-container {
min-height: 100%;
padding-bottom: 20rpx;
}
.message-item {
@ -350,39 +367,39 @@
.scroll-placeholder {
height: 1rpx;
opacity: 0;
}
/* 改进输入区域安全适配 */
.input-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20rpx);
border-top: 1rpx solid rgba(0, 0, 0, 0.08);
z-index: 1000;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
padding: 25rpx;
padding-bottom: calc(25rpx + env(safe-area-inset-bottom));
position: relative;
z-index: 100;
}
/* 功能菜单样式 - 改为绝对定位在输入框上方 */
/* 功能菜单样式 - 调整位置和简化样式 */
.function-menu {
position: absolute;
bottom: 120%;
left: 20rpx; /* 保持左边对齐 */
width: auto; /* 宽度自适应 */
min-width: 400rpx; /* 设置一个最小宽度 */
bottom: 115%; /* 调整位置,离输入框更近 */
left: 20rpx;
width: auto;
min-width: 320rpx; /* 稍微调整宽度 */
height: auto;
display: flex;
flex-direction: column;
padding: 10rpx 0;
background: rgba(255, 255, 255, 0.95);
padding: 0; /* 移除内边距 */
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(76, 175, 80, 0.2);
border-radius: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
border: 1rpx solid rgba(76, 175, 80, 0.3);
border-radius: 16rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
gap: 0;
z-index: 1001;
}
@keyframes slideUp {
@ -400,7 +417,7 @@
display: flex;
flex-direction: row; /* 水平排列 */
align-items: center;
justify-content: flex-start;
justify-content: center; /* 文字居中 */
padding: 25rpx 30rpx;
background: var(--background-white);
transition: all 0.2s ease;
@ -454,7 +471,6 @@
.input-container {
padding: 25rpx;
position: relative;
border-top: 1rpx solid #e0e0e0; /* 添加顶部描边 */
}
.input-box {
@ -464,7 +480,8 @@
border-radius: 50rpx;
padding: 8rpx;
box-shadow: 0 4rpx 25rpx var(--shadow-soft);
border: 2rpx solid var(--border-light);
/* 添加明显的黑色边框 */
border: 2rpx solid #333333;
transition: all 0.3s ease;
}
@ -509,24 +526,30 @@
.message-input {
flex: 1;
font-size: 28rpx;
padding: 15rpx 0;
padding: 25rpx 0; /* 增加垂直内边距使文字居中 */
line-height: 1.4;
color: var(--text-primary);
min-height: 60rpx;
max-height: 200rpx;
resize: none;
overflow-y: auto;
}
/* 发送按钮 - 完全重写确保可见性 */
.send-btn {
width: 70rpx;
height: 70rpx;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 15rpx;
border-radius: 50%;
background: #e8e8e8;
background: #f0f0f0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
border: 2rpx solid #d0d0d0;
}
.send-btn.active {
@ -536,6 +559,7 @@
var(--primary-dark) 100%
);
box-shadow: 0 4rpx 16rpx var(--shadow-light);
border: 2rpx solid var(--primary-dark);
}
.send-btn.active::before {
@ -558,26 +582,33 @@
left: 100%;
}
.send-btn:active {
transform: scale(0.92);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.send-icon {
display: none;
}
.send-icon {
width: 0;
height: 0;
border-top: 18rpx solid transparent;
border-bottom: 18rpx solid transparent;
border-left: 24rpx solid #888;
/* 发送图标 - 确保始终可见 */
.send-icon-image {
width: 36rpx;
height: 36rpx;
transition: all 0.3s ease;
position: relative;
z-index: 2;
transform: translateX(4rpx);
/* 默认状态 - 深灰色确保可见 */
filter: brightness(0) invert(0.3);
}
.send-btn.active .send-icon-image {
/* 激活状态 - 白色 */
filter: brightness(0) invert(1);
}
.send-btn.active .send-icon {
border-left-color: white;
transform: translateX(5rpx);
/* 按钮交互效果 */
.send-btn:not(.active):active {
background: #e0e0e0;
transform: scale(0.95);
}
.send-btn.active:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx var(--shadow-light);
}
.input-status {
@ -631,18 +662,27 @@
justify-content: space-between;
align-items: center;
padding: 30rpx;
background: linear-gradient(
135deg,
var(--primary-medical) 0%,
var(--primary-dark) 100%
);
color: white;
background: #ffffff;
color: #333333;
flex-shrink: 0;
border-bottom: 1rpx solid #f0f0f0;
}
.modal-title {
.modal-title-container {
display: flex;
align-items: center;
}
.modal-title-icon {
font-size: 40rpx;
margin-right: 15rpx;
}
.modal-title-text {
font-size: 32rpx;
font-weight: bold;
color: #000000;
font-family: "Heiti SC", "Microsoft YaHei", sans-serif;
}
.close-btn {
@ -857,6 +897,104 @@
}
}
/* 文件消息样式 */
.file-message {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 20rpx;
}
.file-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
border: 1rpx solid #e9ecef;
margin-bottom: 15rpx;
width: 100%;
}
.file-icon {
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.file-info {
flex: 1;
}
.file-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.file-meta {
font-size: 22rpx;
color: #666;
}
/* 上传进度条 */
.upload-progress {
width: 100%;
margin-top: 15rpx;
}
.progress-bar {
width: 100%;
height: 8rpx;
background: #e9ecef;
border-radius: 4rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(
90deg,
var(--primary-medical),
var(--primary-dark)
);
border-radius: 4rpx;
transition: width 0.3s ease;
}
.progress-text {
font-size: 22rpx;
color: var(--primary-medical);
text-align: right;
margin-top: 8rpx;
}
/* 视频预览样式 */
.video-preview {
width: 100%;
max-width: 400rpx;
border-radius: 12rpx;
overflow: hidden;
margin-bottom: 15rpx;
}
.video-thumbnail {
width: 100%;
height: 200rpx;
background: #000;
display: flex;
align-items: center;
justify-content: center;
}
.video-play-icon {
width: 60rpx;
height: 60rpx;
opacity: 0.8;
}
/* 针对不同屏幕尺寸的适配 */
@media (max-width: 375px) {
.questions-grid {
@ -868,3 +1006,42 @@
max-width: 85%;
}
}
/* 确保容器正确显示 */
.container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f8f9fa;
opacity: 0;
animation: fadeIn 0.5s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 页面加载遮罩 */
.page-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
font-size: 28rpx;
color: var(--primary-medical);
}

@ -43,15 +43,24 @@ Page({
this.refreshFamilyData();
},
loadFamilyData: function () {
const familyData = wx.getStorageSync('familyData') || this.data.familyList;
const totalMembers = familyData.reduce((total, family) => total + (family.members ? family.members.length : 0), 0);
this.setData({
familyList: familyData,
totalFamilies: familyData.length,
totalMembers: totalMembers
});
},
// family.js 中修改 loadFamilyData 方法
loadFamilyData: function () {
// 从缓存读取,若为空则使用默认数据
let familyData = wx.getStorageSync('familyData');
if (!familyData || familyData.length === 0) {
// 使用页面默认的 familyList 作为初始数据
familyData = this.data.familyList;
// 写入缓存,确保其他页面能读到
wx.setStorageSync('familyData', familyData);
}
const totalMembers = familyData.reduce((total, family) => total + (family.members ? family.members.length : 0), 0);
this.setData({
familyList: familyData,
totalFamilies: familyData.length,
totalMembers: totalMembers
});
},
saveFamilyData: function () {
// 保存到本地缓存,确保刷新后数据仍在
@ -197,35 +206,24 @@ Page({
const family = this.data.familyList.find(item => item.id === familyId);
if (!family) return;
wx.showActionSheet({
itemList: ['邀请微信好友', '手动添加成员', '扫描二维码'],
itemList: ['搜索添加成员'],
success: (res) => {
if (res.tapIndex === 0) {
this.inviteWechatFriend(familyId);
} else if (res.tapIndex === 1) {
wx.navigateTo({ url: `/pages/family/add-member/add-member?familyId=${familyId}` });
} else if (res.tapIndex === 2) {
this.scanQRCode(familyId);
}
}
});
},
inviteWechatFriend: function (familyId) {
wx.showShareMenu({ withShareTicket: true });
wx.showToast({ title: '请点击右上角分享', icon: 'none' });
},
scanQRCode: function (familyId) {
wx.scanCode({
success: (res) => {
wx.showToast({ title: '扫描成功', icon: 'success' });
},
fail: (err) => {
wx.showToast({ title: '扫描失败', icon: 'none' });
}
// 跳转到邀请/搜索页面,并传 familyId以后可用于把邀请关联到某家庭
wx.navigateTo({
url: `/pages/family/invite/invite?familyId=${familyId}`
});
},
// ===========================
// 新增:家庭设置入口(齿轮点击)
// ===========================

@ -12,8 +12,6 @@
<text class="stat-label">个家庭</text>
</view>
<view class="stat-item">
<text class="stat-number">{{totalMembers}}</text>
<text class="stat-label">位成员</text>
</view>
</view>
</view>
@ -59,7 +57,7 @@
<view class="create-content">
<view class="create-icon">🏠</view>
<view class="create-text">
<text class="create-title">创建新家庭</text>
<text class="create-title">新家庭</text>
<text class="create-desc">邀请家人一起管理健康</text>
</view>
</view>

@ -0,0 +1,93 @@
// pages/family/invite/invite.js
Page({
data: {
familyId: null,
keyword: '',
searching: false,
// 模拟的用户库(开发时可替换为后端返回的数据)
mockUsers: [
{ id: '10001', nickname: '张三', avatar: '/images/avatars/mock1.png' },
{ id: '10002', nickname: '李四', avatar: '/images/avatars/mock2.png' },
{ id: '10003', nickname: '王五', avatar: '/images/avatars/mock3.png' },
{ id: '10004', nickname: '不吃芹菜', avatar: '/images/avatars/father.png' }
],
results: []
},
onLoad: function (options) {
if (options.familyId) {
this.setData({ familyId: Number(options.familyId) });
}
},
onInput: function (e) {
this.setData({ keyword: e.detail.value });
},
onSearch: function () {
const kw = (this.data.keyword || '').trim();
this.setData({ searching: true, results: [] });
// 简单本地过滤(大小写/部分匹配)
if (!kw) {
// 空关键词不显示结果,直接返回
this.setData({ results: [], searching: true });
return;
}
const lower = kw.toLowerCase();
const results = this.data.mockUsers.filter(u =>
(u.nickname && u.nickname.toLowerCase().includes(lower)) ||
(u.id && String(u.id).toLowerCase().includes(lower))
);
// 模拟网络延迟
setTimeout(() => {
this.setData({ results, searching: true });
if (results.length === 0) {
wx.showToast({ title: '未找到用户', icon: 'none' });
}
}, 350);
},
onSelectUser: function (e) {
// 点击整行也会触发,弹出确认发送邀请
const user = e.currentTarget.dataset.user;
this.confirmAndSendInvite(user);
},
onSendInvite: function (e) {
// 防止事件冒泡导致行 click 双触发
e.stopPropagation();
const user = e.currentTarget.dataset.user;
this.confirmAndSendInvite(user);
},
confirmAndSendInvite: function (user) {
const that = this;
wx.showModal({
title: '发送邀请',
content: `确认向 ${user.nickname}ID${user.id})发送家庭邀请?`,
success(res) {
if (res.confirm) {
that.sendInvite(user);
}
}
});
},
sendInvite: function (user) {
// 这里是发送邀请的前端模拟逻辑
// TODO: 若接入后端/消息接口在这里发请求POST /api/invite {familyId, targetWxid}
wx.showLoading({ title: '发送中...' });
setTimeout(() => {
wx.hideLoading();
wx.showToast({ title: '已发送邀请', icon: 'success' });
// 可选:回到上一级或更新本页状态
// wx.navigateBack();
}, 800);
}
});

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

@ -0,0 +1,38 @@
<!-- pages/family/invite/invite.wxml -->
<view class="page">
<!-- 搜索栏(仿微信样式) -->
<view class="wx-search-bar">
<view class="wx-search-input-box">
<image class="wx-search-icon" src="/images/icons/search.png"></image>
<input
class="wx-search-input"
placeholder="输入昵称或ID号搜索"
placeholder-class="wx-search-placeholder"
bindinput="onInput"
confirm-type="search"
bindconfirm="onSearch"
value="{{keyword}}"
/>
</view>
<view class="wx-search-btn" bindtap="onSearch">搜索</view>
</view>
<view wx:if="{{searching && results.length === 0}}" class="empty">
未找到匹配用户
</view>
<view wx:for="{{results}}" wx:key="id" class="result-item" bindtap="onSelectUser" data-user="{{item}}">
<image class="avatar" src="{{item.avatar}}" mode="aspectFill" />
<view class="info">
<text class="nickname">{{item.nickname}}</text>
<text class="id">{{item.id}}</text>
</view>
<button class="invite-btn" bindtap="onSendInvite" data-user="{{item}}">邀请</button>
</view>
<view class="tips">
提示:当前为本地模拟搜索。
</view>
</view>

@ -0,0 +1,110 @@
/* 整体页面 */
.page {
padding: 0;
background: #f5f6f7;
min-height: 100vh;
}
/* ========== 微信风格搜索栏 ========== */
.wx-search-bar {
display: flex;
align-items: center;
padding: 10px 12px;
background: #ffffff;
border-bottom: 1px solid #eee;
}
.wx-search-input-box {
flex: 1;
display: flex;
align-items: center;
background: #f2f2f2;
border-radius: 999px; /* 和微信一样的巨大圆角 */
padding: 6px 12px;
}
.wx-search-icon {
width: 18px;
height: 18px;
opacity: 0.6;
margin-right: 6px;
}
.wx-search-input {
flex: 1;
height: 34px;
font-size: 15px;
background: transparent;
}
.wx-search-placeholder {
color: #b5b5b5;
}
.wx-search-btn {
margin-left: 10px;
padding: 6px 16px;
background: #1aad19;
color: #fff;
border-radius: 8px;
font-size: 15px;
}
/* ========== 搜索结果 ========== */
.result-item {
display: flex;
align-items: center;
padding: 12px;
background: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 12px;
}
.info {
flex: 1;
}
.nickname {
display: block;
font-size: 16px;
font-weight: 500;
color: #333;
}
.wxid {
display: block;
font-size: 12px;
color: #888;
margin-top: 4px;
}
.invite-btn {
padding: 6px 14px;
background: #1aad19;
color: #fff;
font-size: 14px;
border-radius: 6px;
}
/* 空状态 */
.empty {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
/* 底部提示 */
.tips {
text-align: center;
font-size: 12px;
color: #999;
padding: 20px 0;
}

@ -0,0 +1,48 @@
// family-members.js
Page({
data: {
familyList: []
},
// family-members.js 中修改 onLoad 方法
onLoad() {
let familyList = wx.getStorageSync('familyData');
// 若缓存为空,从默认数据初始化(可从 family.js 的默认数据复制一份作为备用)
if (!familyList || familyList.length === 0) {
// 这里的默认数据应与 family.js 中的默认 familyList 保持一致
familyList = [
{
id: 1,
name: '温馨小家',
description: '共同关注家人健康',
members: [
{ id: 1, role: '爸爸', nickname: '不吃芹菜', avatar: '/images/avatars/father.png' },
{ id: 2, role: '爷爷', nickname: '最爱打麻将', avatar: '/images/avatars/grandfather.png' },
{ id: 3, role: '女儿', nickname: '爱吃香菜', avatar: '/images/avatars/daughter.png' }
]
},
{
id: 2,
name: '健康之家',
description: '科学管理全家健康',
members: [
{ id: 4, role: '妈妈', nickname: '健康达人', avatar: '/images/avatars/mother.png' },
{ id: 5, role: '儿子', nickname: '运动小将', avatar: '/images/avatars/son.png' }
]
}
];
// 写入缓存,避免下次加载仍为空
wx.setStorageSync('familyData', familyList);
}
this.setData({ familyList });
},
selectMember(e) {
const member = e.currentTarget.dataset.member;
if (!member || !member.id) {
wx.showToast({ title: '成员信息异常', icon: 'none' });
return;
}
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('selectMember', member);
wx.navigateBack();
}
});

@ -0,0 +1,3 @@
{
"navigationBarTitleText": "选择家庭成员"
}

@ -0,0 +1,21 @@
<view class="page-container">
<view class="section-title">请选择要查看的家庭成员</view>
<!-- 遍历家庭列表 -->
<block wx:for="{{familyList}}" wx:key="id">
<view class="family-block">
<text class="family-name">{{item.name}}</text>
<!-- 遍历成员列表 -->
<block wx:for="{{item.members}}" wx:key="id">
<view class="member-item" bindtap="selectMember" data-member="{{item}}">
<image src="{{item.avatar}}" class="avatar" />
<view class="info">
<text class="nickname">{{item.nickname}}</text>
<text class="role">{{item.role}}</text>
</view>
</view>
</block>
</view>
</block>
</view>

@ -0,0 +1,54 @@
.page-container {
padding: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 20rpx;
color: #333;
}
.family-block {
margin-bottom: 40rpx;
}
.family-name {
font-size: 28rpx;
font-weight: bold;
color: #4CAF50;
margin-bottom: 10rpx;
display: block;
}
.member-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.05);
margin-bottom: 16rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.info {
display: flex;
flex-direction: column;
}
.nickname {
font-size: 28rpx;
font-weight: 500;
}
.role {
font-size: 24rpx;
color: #888;
}

@ -3,198 +3,280 @@ Page({
currentData: 'mine',
currentDate: '2025-10-18',
currentChart: 'bloodSugar',
titleText: '我的数据', // ✅ 新增,用于动态显示“爸爸的数据”
selectedMember: null, // ✅ 当前选中的成员信息
showDeviceModal: false, // 设备模态框是否显示
currentDevice: '', // 当前选中的设备
today: '', // 今天的日期
showDatePicker: false ,// 日期选择器是否显示
// 我的数据
dataList: [
{
id: 1,
date: '2025-08-07',
time: '19:42',
bloodSugar: '5.7',
bloodOxygen: '96',
heartRate: '66'
},
{
id: 2,
date: '2024-03-10',
time: '20:06',
bloodSugar: '7.0',
bloodOxygen: '95',
heartRate: '82'
},
{
id: 3,
date: '2025-10-01',
time: '14:11',
bloodSugar: '7.9',
bloodOxygen: '97',
heartRate: '76'
}
{ id: 1, date: '2025-08-07', time: '19:42', bloodSugar: '5.7', bloodOxygen: '96', heartRate: '66' },
{ id: 2, date: '2024-03-10', time: '20:06', bloodSugar: '7.0', bloodOxygen: '95', heartRate: '82' },
{ id: 3, date: '2025-10-01', time: '14:11', bloodSugar: '7.9', bloodOxygen: '97', heartRate: '76' }
],
familyDataList: [
{
id: 1,
date: '2025-08-07',
time: '19:42',
bloodSugar: '4.5',
bloodOxygen: '95',
heartRate: '74'
}
// 其他家人数据...
]
// 家人假数据可按成员ID加载
familyHistoryMock: {
1: [ // 爸爸
{ id: 101, date: '2025-08-07', time: '08:00', bloodSugar: '6.2', bloodOxygen: '95', heartRate: '72' },
{ id: 102, date: '2025-10-11', time: '18:30', bloodSugar: '7.1', bloodOxygen: '94', heartRate: '78' }
],
2: [ // 妈妈
{ id: 201, date: '2025-07-09', time: '07:50', bloodSugar: '5.5', bloodOxygen: '97', heartRate: '70' },
{ id: 202, date: '2025-09-14', time: '21:00', bloodSugar: '6.4', bloodOxygen: '96', heartRate: '74' }
]
}
},
onLoad: function () {
this.loadHistoryData();
this.drawChart();
deviceEntry() {
this.setData({
showDeviceModal: true,
currentDevice: '' // 重置选中状态
});
},
onShow() {
this.loadHistoryData();
// 选择设备(单选逻辑)
selectDevice(e) {
const device = e.currentTarget.dataset.device;
this.setData({ currentDevice: device });
},
// 新增:关闭模态框
closeDeviceModal() {
this.setData({
showDeviceModal: false,
currentDevice: '' // 重置选中状态
});
},
// 确认导入
confirmImport() {
if (!this.data.currentDevice) {
// 未选择设备时提示
wx.showToast({
title: '请选择设备',
icon: 'none',
duration: 1500
});
return;
}
loadHistoryData: function () {
// 模拟导入成功(实际项目中可对接后端接口)
wx.showToast({
title: '导入成功',
icon: 'success',
duration: 1500
});
// 关闭模态框
this.setData({ showDeviceModal: false });
},
onLoad() {
// 初始化日期为今天
const today = this.formatDate(new Date());
this.setData({ today, currentDate: today });
},
onShow() {
if (this.data.selectedMember) {
this.loadMemberHistory(this.data.selectedMember);
} else {
this.loadHistoryData();
}
// 确保图表刷新如果上述方法未调用drawChart这里补充
this.drawChart();
},
// 加载“我的数据”
loadHistoryData() {
const historyData = wx.getStorageSync('historyData') || this.data.dataList;
this.setData({ dataList: historyData });
this.setData({ dataList: historyData, titleText: '我的数据' });
},
switchData: function (e) {
const type = e.currentTarget.dataset.type;
this.setData({
currentData: type,
dataList: type === 'mine' ? this.data.dataList : this.data.familyDataList
// ✅ 加载成员假数据
// history.js 中修改 loadMemberHistory 方法
loadMemberHistory(member) {
const mockData = this.data.familyHistoryMock[member.id] || [];
// 从缓存读取真实数据(若有)
const cacheData = wx.getStorageSync(`member_${member.id}_history`) || [];
const realData = cacheData.length > 0 ? cacheData : mockData;
if (realData.length === 0) {
wx.showToast({ title: '暂无该成员数据', icon: 'none' });
}
this.setData({
dataList: realData,
titleText: `${member.role}的数据`,
currentData: 'family'
});
this.drawChart();
},
// ✅ 切换数据按钮 → 跳转到家人选择页
switchToFamilyMembers() {
const that = this;
wx.navigateTo({
url: '/pages/history/family-members',
events: {
selectMember(member) {
// 接收 family-members 页返回的成员对象
that.setData({ selectedMember: member });
that.loadMemberHistory(member);
}
},
success(res) {
// 在目标页面注册事件通道
res.eventChannel.emit('openFromHistory', {});
}
});
this.drawChart();
},
switchChart: function (e) {
switchChart(e) {
const type = e.currentTarget.dataset.type;
this.setData({ currentChart: type });
this.drawChart();
},
showCalendar: function () {
wx.navigateTo({
url: '/pages/history/calendar/calendar'
});
},
prevDate: function () {
// 切换到前一天
prevDate() {
const current = new Date(this.data.currentDate);
current.setDate(current.getDate() - 1);
this.setData({
currentDate: this.formatDate(current)
});
this.loadDateData();
this.setData({ currentDate: this.formatDate(current) });
},
nextDate: function () {
// 切换到后一天
nextDate() {
const current = new Date(this.data.currentDate);
current.setDate(current.getDate() + 1);
this.setData({
currentDate: this.formatDate(current)
});
this.loadDateData();
this.setData({ currentDate: this.formatDate(current) });
},
// 选择日期后触发
onDateSelect(e) {
const selectedDate = e.detail.value;
this.setData({
currentDate: selectedDate,
showDatePicker: false // 选择后关闭选择器
});
// 加载选中日期的数据
this.loadDataByDate(selectedDate);
},
// 加载对应日期的数据(根据实际接口修改)
loadDataByDate(date) {
console.log('加载日期数据:', date);
// 实际项目中调用接口:
// wx.request({
// url: `/api/history?date=${date}`,
// success: (res) => { /* 更新数据展示 */ }
// });
},
formatDate: function (date) {
// 格式化日期为 YYYY-MM-DD
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
loadDateData: function () {
// 加载指定日期的数据
wx.showLoading({ title: '加载中...' });
// 模拟API调用
setTimeout(() => {
wx.hideLoading();
}, 500);
showCalendar() {
this.setData({ showDatePicker: true });
},
manualEntry: function () {
wx.navigateTo({
url: '/pages/history/manual-entry/manual-entry'
});
manualEntry() {
wx.navigateTo({ url: '/pages/history/manual-entry/manual-entry' });
},
drawChart: function () {
const ctx = wx.createCanvasContext('dataChart');
const data = this.getChartData();
// 清除画布
ctx.clearRect(0, 0, 300, 200);
// 绘制坐标轴
ctx.setStrokeStyle('#e0e0e0');
ctx.setLineWidth(1);
ctx.moveTo(50, 20);
ctx.lineTo(50, 180);
ctx.lineTo(280, 180);
ctx.stroke();
// 绘制数据线
ctx.setStrokeStyle('#4CAF50');
ctx.setLineWidth(2);
// 替换原drawChart方法
drawChart() {
const ctx = wx.createCanvasContext('dataChart');
const data = this.getChartData();
const type = this.data.currentChart;
// 清除画布
ctx.clearRect(0, 0, 300, 200);
// 绘制坐标轴
ctx.setStrokeStyle('#e0e0e0');
ctx.moveTo(50, 20); // 纵轴顶部
ctx.lineTo(50, 180); // 纵轴底部
ctx.lineTo(280, 180); // 横轴右端
ctx.stroke();
// 根据类型设置样式(与模拟数据匹配)
const config = {
bloodSugar: { color: '#4CAF50' }, // 血糖
bloodOxygen: { color: '#2196F3' }, // 血氧
heartRate: { color: '#FF9800' } // 心率
};
const { color } = config[type];
// 绘制数据线
ctx.setStrokeStyle(color);
ctx.setLineWidth(2);
ctx.beginPath();
data.forEach((point, i) => {
const x = 50 + (i * 40); // 横轴平均分布
// 针对模拟数据范围优化纵轴位置(固定缩放比例)
const y = type === 'bloodSugar'
? 180 - (point.value * 15) // 血糖5-8 → 缩放15倍
: type === 'bloodOxygen'
? 180 - ((point.value - 90) * 10) // 血氧95-97 → 基于90偏移
: 180 - ((point.value - 60) * 3); // 心率65-82 → 基于60偏移
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
// 绘制数据点
ctx.setFillStyle(color);
data.forEach((point, i) => {
const x = 50 + (i * 40);
const y = type === 'bloodSugar'
? 180 - (point.value * 15)
: type === 'bloodOxygen'
? 180 - ((point.value - 90) * 10)
: 180 - ((point.value - 60) * 3);
ctx.beginPath();
data.forEach((point, index) => {
const x = 50 + (index * 40);
const y = 180 - (point.value * 2);
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// 绘制数据点
ctx.setFillStyle('#4CAF50');
data.forEach((point, index) => {
const x = 50 + (index * 40);
const y = 180 - (point.value * 2);
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
ctx.draw();
},
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
ctx.draw();
},
getChartData: function () {
// 根据当前选择的图表类型返回数据
const type = this.data.currentChart;
const dataMap = {
bloodSugar: [
{ time: '06:00', value: 5.3 },
{ time: '09:00', value: 7.8 },
{ time: '12:00', value: 6.2 },
{ time: '15:00', value: 5.9 },
{ time: '18:00', value: 7.5 },
{ time: '21:00', value: 6.1 }
],
bloodOxygen: [
{ time: '06:00', value: 95 },
{ time: '09:00', value: 96 },
{ time: '12:00', value: 97 },
{ time: '15:00', value: 95 },
{ time: '18:00', value: 96 },
{ time: '21:00', value: 95 }
],
heartRate: [
{ time: '06:00', value: 65 },
{ time: '09:00', value: 78 },
{ time: '12:00', value: 82 },
{ time: '15:00', value: 75 },
{ time: '18:00', value: 80 },
{ time: '21:00', value: 68 }
]
};
return dataMap[type] || dataMap.bloodSugar;
}
});
// 替换原getChartData方法
getChartData() {
const type = this.data.currentChart;
// 不同类型的模拟数据(固定值,无需关联实际数据)
const mockData = {
bloodSugar: [
{ time: '06:00', value: 5.3 },
{ time: '09:00', value: 7.8 },
{ time: '12:00', value: 6.2 },
{ time: '15:00', value: 5.9 },
{ time: '18:00', value: 7.5 },
{ time: '21:00', value: 6.1 }
],
bloodOxygen: [
{ time: '06:00', value: 95 },
{ time: '09:00', value: 96 },
{ time: '12:00', value: 97 },
{ time: '15:00', value: 95 },
{ time: '18:00', value: 96 },
{ time: '21:00', value: 95 }
],
heartRate: [
{ time: '06:00', value: 65 },
{ time: '09:00', value: 78 },
{ time: '12:00', value: 82 },
{ time: '15:00', value: 75 },
{ time: '18:00', value: 80 },
{ time: '21:00', value: 68 }
]
};
return mockData[type] || mockData.bloodSugar;
}
});

@ -1,27 +1,36 @@
<view class="container">
<!-- 数据选择器 -->
<view class="data-selector card">
<view class="selector-item {{currentData === 'mine' ? 'active' : ''}}"
bindtap="switchData" data-type="mine">
我的数据
</view>
<view class="selector-item {{currentData === 'family' ? 'active' : ''}}"
bindtap="switchData" data-type="family">
家人数据
</view>
<view class="tab-bar">
<view class="tab {{currentData === 'mine' ? 'active' : ''}}">
{{titleText}}
</view>
<view class="tab" bindtap="switchToFamilyMembers">
切换数据
</view>
</view>
<!-- 日期选择 -->
<view class="date-section card">
<view class="date-display">
<text class="current-date">{{currentDate}}</text>
<text class="calendar-icon" bindtap="showCalendar">📅</text>
</view>
<view class="date-navigation">
<text class="nav-btn" bindtap="prevDate">前一天</text>
<text class="nav-btn" bindtap="nextDate">后一天</text>
</view>
<!-- 日期选择区域 -->
<view class="date-section card">
<view class="date-display">
<text class="current-date">{{currentDate}}</text>
<!-- 日历图标作为日期选择触发按钮直接包裹在picker中 -->
<picker
mode="date"
value="{{currentDate}}"
start="2020-01-01"
end="{{today}}"
bindchange="onDateSelect"
>
<text class="calendar-icon">📅</text> <!-- 点击图标直接弹出选择器 -->
</picker>
</view>
<view class="date-navigation">
<!-- 只保留“前一天”“后一天”按钮,删除“选择”按钮 -->
<text class="nav-btn" bindtap="prevDate">前一天</text>
<text class="nav-btn" bindtap="nextDate">后一天</text>
</view>
</view>
<!-- 手动录入 -->
<view class="manual-entry card" bindtap="manualEntry">
@ -30,6 +39,57 @@
<text class="entry-arrow">></text>
</view>
<view class="device-entry card" bindtap="deviceEntry">
<image src="/images/icons/device.png" class="entry-icon"></image>
<text class="entry-text">批量导入数据</text>
<text class="entry-arrow">></text>
</view>
<!-- 设备选择模态框(默认隐藏) -->
<view class="modal-mask" wx:if="{{showDeviceModal}}"></view>
<view class="device-modal" wx:if="{{showDeviceModal}}">
<!-- 新增:右上角关闭按钮 -->
<view class="modal-close" bindtap="closeDeviceModal">×</view>
<!-- 模态框标题 -->
<view class="modal-title">选择设备</view>
<!-- 设备列表 -->
<view class="device-list">
<view class="device-item" bindtap="selectDevice" data-device="华为手表Watch GT3">
<view class="device-info">
<image src="/images/devices/huawei.png" class="device-img"></image>
<text class="device-name">华为手表Watch GT3</text>
</view>
<view class="device-check {{currentDevice === '华为手表Watch GT3' ? 'checked' : ''}}">
<text class="check-icon" wx:if="{{currentDevice === '华为手表Watch GT3'}}">✓</text>
</view>
</view>
<view class="device-item" bindtap="selectDevice" data-device="小米血糖仪">
<view class="device-info">
<image src="/images/devices/xiaomi.png" class="device-img"></image>
<text class="device-name">小米血糖仪</text>
</view>
<view class="device-check {{currentDevice === '小米血糖仪' ? 'checked' : ''}}">
<text class="check-icon" wx:if="{{currentDevice === '小米血糖仪'}}">✓</text>
</view>
</view>
<view class="device-item" bindtap="selectDevice" data-device="苹果健康">
<view class="device-info">
<image src="/images/devices/apple.png" class="device-img"></image>
<text class="device-name">苹果健康</text>
</view>
<view class="device-check {{currentDevice === '苹果健康' ? 'checked' : ''}}">
<text class="check-icon" wx:if="{{currentDevice === '苹果健康'}}">✓</text>
</view>
</view>
</view>
<!-- 确认按钮 -->
<button class="confirm-btn" bindtap="confirmImport">确认导入</button>
</view>
<!-- 数据表格 -->
<view class="data-table card">
<view class="table-header">
@ -38,6 +98,8 @@
<text class="header-cell">血氧 %</text>
<text class="header-cell">心率 BPM</text>
</view>
<view class="table-body">
<view class="table-row" wx:for="{{dataList}}" wx:key="id">

@ -59,6 +59,21 @@
margin-left: 15rpx;
}
.nav-btn:active {
background-color: #e5e5e5; /* 点击反馈 */
}
/* 日期选择器占位层(不影响布局) */
.date-picker-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99;
opacity: 0; /* 透明仅用于触发picker */
}
.manual-entry {
display: flex;
align-items: center;
@ -190,4 +205,151 @@
.chart-canvas {
width: 100%;
height: 300rpx;
}
/* 入口组件样式(手动录入和批量导入通用) */
.device-entry {
display: flex;
align-items: center;
padding: 30rpx;
margin-bottom: 30rpx;
}
/* 模态框遮罩层 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 999;
}
/* 关闭按钮样式 */
.modal-close {
position: absolute;
top: 30rpx;
right: 30rpx;
font-size: 40rpx;
color: #999; /* 灰色,不突兀但清晰 */
}
.modal-close:active {
color: #666; /* 点击时加深 */
}
/* 设备选择模态框 */
.device-modal {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #fff;
border-top-left-radius: 30rpx;
border-top-right-radius: 30rpx;
z-index: 1000;
padding: 30rpx;
box-sizing: border-box;
padding-top: 50rpx; /* 顶部留空间给关闭按钮 */
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
text-align: center;
}
/* 设备列表 */
.device-list {
margin-bottom: 40rpx;
}
.device-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.device-info {
display: flex;
align-items: center;
}
.device-img {
width: 70rpx;
height: 70rpx;
margin-right: 24rpx;
border-radius: 10rpx;
}
.device-name {
font-size: 30rpx;
color: #333;
}
/* 选择框样式 */
.device-check {
width: 44rpx;
height: 44rpx;
border: 2rpx solid #ccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.device-check.checked {
background-color: #07c160;
border-color: #07c160;
}
.check-icon {
color: #fff;
font-size: 28rpx;
}
/* 确认按钮 */
.confirm-btn {
width: 100%;
padding: 26rpx 0;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
border-radius: 28rpx;
border: none;
}
.confirm-btn::after {
border: none;
}
.confirm-btn:active {
background-color: #06b356;
}
/* 选择器的触发区域:始终占满屏幕,透明但可点击 */
.picker-trigger {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999; /* 确保在最顶层,覆盖其他元素 */
background-color: rgba(0,0,0,0.01); /* 轻微透明背景,确保可点击(避免完全透明时部分设备不响应) */
}
/* 遮罩层:全屏半透明,点击关闭选择器 */
.date-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.3); /* 半透明黑色,区分弹窗内外 */
z-index: 998;
}

@ -1,66 +0,0 @@
// pages/history/manual-entry.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

@ -1,2 +0,0 @@
<!--pages/history/manual-entry.wxml-->
<text>pages/history/manual-entry.wxml</text>

@ -0,0 +1,120 @@
// member-history.js
Page({
data: {
memberId: '', // 成员ID
memberName: '', // 成员名称
currentDate: '', // 当前日期
dataType: '血糖', // 当前数据类型
historyList: [] // 成员的历史数据
},
onLoad(options) {
// 接收传递的成员ID和名称
const { memberId, memberName } = options;
this.setData({ memberId, memberName });
// 初始化日期(默认今天)
this.setData({ currentDate: this.formatDate(new Date()) });
// 加载该成员的历史数据
this.loadMemberHistory();
},
// 加载成员历史数据(从专属缓存读取)
loadMemberHistory() {
const { memberId, currentDate } = this.data;
// 缓存键使用修正后的memberId此时已正确传递成员的id
const allHistory = wx.getStorageSync(`member_${memberId}_history`) || [];
const filteredData = allHistory.filter(item => item.date === currentDate);
this.setData({ historyList: filteredData });
this.drawChart(filteredData);
},
// 复用history.js的其他方法切换日期、切换数据类型等
formatDate(date) { /* 与history.js一致 */ },
prevDate() { /* 与history.js一致 */ },
nextDate() { /* 与history.js一致 */ },
switchChart(e) {
const type = e.currentTarget.dataset.type;
this.setData({ currentChart: type });
this.drawChart();
},
drawChart() {
const ctx = wx.createCanvasContext('dataChart');
const type = this.data.currentChart || 'bloodSugar'; // 默认血糖
// 模拟数据(与 history.js 保持一致)
const mockData = {
bloodSugar: [
{ time: '06:00', value: 5.3 },
{ time: '09:00', value: 7.8 },
{ time: '12:00', value: 6.2 },
{ time: '15:00', value: 5.9 },
{ time: '18:00', value: 7.5 },
{ time: '21:00', value: 6.1 }
],
bloodOxygen: [
{ time: '06:00', value: 95 },
{ time: '09:00', value: 96 },
{ time: '12:00', value: 97 },
{ time: '15:00', value: 95 },
{ time: '18:00', value: 96 },
{ time: '21:00', value: 95 }
],
heartRate: [
{ time: '06:00', value: 65 },
{ time: '09:00', value: 78 },
{ time: '12:00', value: 82 },
{ time: '15:00', value: 75 },
{ time: '18:00', value: 80 },
{ time: '21:00', value: 68 }
]
};
const data = mockData[type];
// 清除画布
ctx.clearRect(0, 0, 300, 200);
// 绘制坐标轴(同 history.js
ctx.setStrokeStyle('#e0e0e0');
ctx.moveTo(50, 20);
ctx.lineTo(50, 180);
ctx.lineTo(280, 180);
ctx.stroke();
// 样式配置
const colorMap = {
bloodSugar: '#4CAF50',
bloodOxygen: '#2196F3',
heartRate: '#FF9800'
};
ctx.setStrokeStyle(colorMap[type]);
ctx.setLineWidth(2);
// 绘制数据线和点(同 history.js 逻辑)
ctx.beginPath();
data.forEach((point, i) => {
const x = 50 + (i * 40);
const y = type === 'bloodSugar'
? 180 - (point.value * 15)
: type === 'bloodOxygen'
? 180 - ((point.value - 90) * 10)
: 180 - ((point.value - 60) * 3);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
ctx.setFillStyle(colorMap[type]);
data.forEach((point, i) => {
const x = 50 + (i * 40);
const y = type === 'bloodSugar'
? 180 - (point.value * 15)
: type === 'bloodOxygen'
? 180 - ((point.value - 90) * 10)
: 180 - ((point.value - 60) * 3);
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
ctx.draw();
},
// ...其他方法
})

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "成员历史数据",
"enablePullDownRefresh": true,
"backgroundColor": "#f8fbff",
"backgroundTextStyle": "dark",
"onReachBottomDistance": 50
}

@ -0,0 +1,6 @@
<!-- 复用history.wxml的所有结构顶部可增加成员名称展示 -->
<view class="member-title">{{memberName}}的历史数据</view>
<!-- 以下内容与history.wxml完全一致日期选择器、图表、数据列表等 -->
<view class="date-container">...</view>
<view class="chart-container">...</view>
<view class="data-list">...</view>

@ -0,0 +1,18 @@
/* 导入history主页面的样式同一目录下直接引用 */
@import "./history.wxss";
/* 成员标题特有样式(展示在页面顶部) */
.member-title {
padding: 20rpx 30rpx;
font-size: 32rpx;
color: #333;
border-bottom: 1rpx solid #eee;
margin-bottom: 20rpx;
font-weight: 500;
}
/* 若需要调整成员数据列表的样式可在此处覆盖history.wxss的样式 */
/* 例如:调整成员数据项的高亮色 */
.data-item.highlight {
background-color: #e8f4fd; /* 与“我的数据”区分的浅蓝色背景 */
}

@ -9,13 +9,42 @@ Page({
heartRate: '78',
heartRateTrend: 'stable',
heartRateTrendText: '稳定',
bloodOxygen: '96'
bloodOxygen: '96',
timeRange: '24h', // 默认显示24小时数据
avgSugar: 0,
measureCount: 0,
todayStatus: '',
// 不同时间范围的血糖数据
sugarData: {
'24h': {
times: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'],
values: [5.1, 4.9, 5.3, 7.8, 6.2, 5.9, 7.5, 6.1]
},
'7d': {
times: ['10/12', '10/13', '10/14', '10/15', '10/16', '10/17', '10/18'],
values: [5.3, 5.5, 6.1, 5.8, 5.2, 5.4, 5.3]
},
'30d': {
times: ['9/18', '9/22', '9/26', '9/30', '10/4', '10/8', '10/12', '10/16', '10/18'],
values: [5.4, 5.6, 5.8, 6.0, 5.7, 5.5, 5.3, 5.4, 5.3]
}
}
},
// 切换时间范围
changeTimeRange: function(e) {
const range = e.currentTarget.dataset.range;
this.setData({ timeRange: range }, () => {
this.drawSugarChart();
this.calculateStats(); // 添加这行
});
},
onLoad: function () {
this.loadUserInfo();
this.setCurrentDate();
this.drawSugarChart();
this.calculateStats(); // 添加这行
},
onShow: function () {
@ -66,10 +95,61 @@ Page({
return `${hours}:${minutes}`;
},
// 在index.js中添加
calculateStats: function() {
const { timeRange, sugarData } = this.data;
const { values } = sugarData[timeRange];
// 计算测量次数
const measureCount = values.length;
// 计算平均血糖
const sum = values.reduce((acc, val) => acc + val, 0);
const avgSugar = (sum / measureCount).toFixed(1);
// 确定今日状态(根据血糖值范围判断)
let todayStatus = '';
let statusClass = '';
const latestValue = values[values.length - 1];
if (latestValue < 3.9) {
todayStatus = '低血糖';
statusClass = 'error';
} else if (latestValue <= 7.2) {
todayStatus = '正常';
statusClass = 'success';
} else if (latestValue <= 10) {
todayStatus = '轻度升高';
statusClass = 'warning';
} else {
todayStatus = '明显升高';
statusClass = 'error';
}
this.setData({
avgSugar,
measureCount,
todayStatus,
statusClass
});
},
// 重写绘制图表方法,支持不同时间范围
drawSugarChart: function () {
const ctx = wx.createCanvasContext('sugarChart');
const width = 600;
const { timeRange, sugarData } = this.data;
const { times, values } = sugarData[timeRange];
const pointCount = times.length;
// ✅ 在这里动态计算宽度
const width = Math.max(600, pointCount * 120);
const height = 200;
this.setData({ chartWidth: width });
const maxValue = 10; // 血糖最大值参考值
// 清除画布
ctx.clearRect(0, 0, width, height + 40);
// 绘制背景网格
ctx.setStrokeStyle('#e0e0e0');
@ -82,27 +162,31 @@ Page({
ctx.lineTo(width - 20, y);
}
// 垂直网格线(时间点
const times = ['06:00', '09:00', '12:00', '15:00', '18:00', '21:00'];
// 垂直网格线(根据数据点数量动态生成
const step = (width - 70) / (pointCount - 1);
times.forEach((time, index) => {
const x = 50 + index * 100;
const x = 50 + index * step;
ctx.moveTo(x, 40);
ctx.lineTo(x, 200);
});
ctx.stroke();
// 绘制纵轴标签(血糖值单位)
ctx.setFontSize(14);
ctx.setFillStyle('#666');
ctx.fillText('血糖值 (mmol/L)', 10, 35); // 纵轴上方
ctx.fillText('时间 ', 70, 233); // 横轴下方
// 绘制数据线
const data = [5.1, 7.8, 6.2, 5.9, 7.5, 6.1];
const maxValue = 10;
ctx.setStrokeStyle('#2196F3');
ctx.setLineWidth(3);
ctx.setLineCap('round');
ctx.setLineJoin('round');
ctx.beginPath();
data.forEach((value, index) => {
const x = 50 + index * 100;
values.forEach((value, index) => {
const x = 50 + index * step;
const y = 200 - (value / maxValue) * 160;
if (index === 0) {
@ -115,8 +199,8 @@ Page({
// 绘制数据点
ctx.setFillStyle('#2196F3');
data.forEach((value, index) => {
const x = 50 + index * 100;
values.forEach((value, index) => {
const x = 50 + index * step;
const y = 200 - (value / maxValue) * 160;
ctx.beginPath();
@ -130,7 +214,13 @@ Page({
// 绘制时间标签
ctx.setFillStyle('#999');
ctx.fillText(times[index], x - 20, 220);
ctx.setFontSize(16);
//时间
if (timeRange === '30d') {
ctx.fillText(times[index], x - (times[index].length * 8), 220);
} else {
ctx.fillText(times[index], x - (times[index].length * 8), 220);
}
});
ctx.draw();
@ -160,8 +250,11 @@ Page({
});
},
onPullDownRefresh: function () {
this.loadHealthData();
wx.stopPullDownRefresh();
}
});
});

@ -77,26 +77,34 @@
<view class="section-header flex-between">
<text class="section-title">血糖趋势</text>
<view class="time-filter">
<text class="filter-item active">24h</text>
<text class="filter-item">7天</text>
<text class="filter-item">30天</text>
</view>
</view>
<canvas canvas-id="sugarChart" class="sugar-chart"></canvas>
<view class="chart-stats">
<view class="stat-item">
<text class="stat-value">5.3</text>
<text class="stat-label">平均血糖</text>
</view>
<view class="stat-item">
<text class="stat-value">2</text>
<text class="stat-label">测量次数</text>
</view>
<view class="stat-item">
<text class="stat-value success">正常</text>
<text class="stat-label">今日状态</text>
</view>
<text class="filter-item {{timeRange === '24h' ? 'active' : ''}}" bindtap="changeTimeRange" data-range="24h">24h
</text>
<text class="filter-item {{timeRange === '7d' ? 'active' : ''}}" bindtap="changeTimeRange" data-range="7d">7天
</text>
<text class="filter-item {{timeRange === '30d' ? 'active' : ''}}" bindtap="changeTimeRange" data-range="30d">30天
</text>
</view>
</view>
<!-- 可横向滚动的趋势图 -->
<scroll-view scroll-x="true" class="chart-scroll">
<canvas canvas-id="sugarChart" class="sugar-chart" style="width: {{chartWidth}}px; height: 250px;"></canvas>
</scroll-view>
<!-- 修改chart-stats部分 -->
<view class="chart-stats">
<view class="stat-item">
<text class="stat-value">{{avgSugar}}</text>
<text class="stat-label">平均血糖</text>
</view>
<view class="stat-item">
<text class="stat-value">{{measureCount}}</text>
<text class="stat-label">测量次数</text>
</view>
<view class="stat-item">
<text class="stat-value {{statusClass}}">{{todayStatus}}</text>
<text class="stat-label">今日状态</text>
</view>
</view>
</view>
<!-- 今日提醒 -->

@ -278,4 +278,15 @@
.reminder-status.completed {
background: var(--success-color);
}
}
.chart-scroll {
width: 100%;
overflow-x: scroll;
white-space: nowrap;
}
.sugar-chart {
width: 1000px; /* 初始宽度会被JS动态修改 */
height: 250px;
}

@ -0,0 +1,30 @@
<view class="container">
<!-- 文章标题 -->
<view class="article-header">
<text class="article-title">{{articleDetail.title}}</text>
<view class="article-meta">
<text class="author">作者:{{articleDetail.author}}</text>
<text class="date">{{articleDetail.date}}</text>
<text class="read-count">{{articleDetail.readCount}} 阅读</text>
</view>
</view>
<!-- 文章封面 -->
<image wx:if="{{articleDetail.cover}}" src="{{articleDetail.cover}}" class="article-cover" mode="widthFix"></image>
<!-- 文章内容 -->
<view class="article-content">
<text class="content-text">{{articleDetail.content}}</text>
</view>
<!-- 相关推荐 -->
<view class="related-articles" wx:if="{{relatedArticles.length > 0}}">
<view class="section-title">相关推荐</view>
<view class="related-list">
<view class="related-item" wx:for="{{relatedArticles}}" wx:key="id" bindtap="viewRelatedArticle" data-id="{{item.id}}">
<text class="related-title">{{item.title}}</text>
<text class="related-read">{{item.readCount}} 阅读</text>
</view>
</view>
</view>
</view>

@ -0,0 +1,83 @@
.container {
padding: 30rpx;
background: #f8f8f8;
min-height: 100vh;
}
.article-header {
margin-bottom: 40rpx;
}
.article-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
display: block;
margin-bottom: 20rpx;
}
.article-meta {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #999;
}
.article-cover {
width: 100%;
border-radius: 10rpx;
margin-bottom: 40rpx;
}
.article-content {
background: #fff;
padding: 40rpx;
border-radius: 10rpx;
margin-bottom: 40rpx;
}
.content-text {
font-size: 30rpx;
line-height: 1.8;
color: #333;
}
.related-articles {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
display: block;
}
.related-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.related-title {
flex: 1;
font-size: 28rpx;
color: #333;
margin-right: 20rpx;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.related-read {
font-size: 24rpx;
color: #999;
white-space: nowrap;
}

@ -1,7 +1,6 @@
Page({
data: {
currentTime: '12:00',
appName: '智慧管理',
appName: 'GlucoWell',
selectedIndex: 0,
userList: [
{
@ -21,11 +20,6 @@ Page({
appName: options.appName
})
}
this.updateTime()
this.timer = setInterval(() => {
this.updateTime()
}, 60000)
},
onUnload() {
@ -33,16 +27,6 @@ Page({
clearInterval(this.timer)
}
},
updateTime() {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
this.setData({
currentTime: `${hours}:${minutes}`
})
},
selectUser(e) {
const index = e.currentTarget.dataset.index
this.setData({

@ -1,4 +1,3 @@
{
"usingComponents": {},
"navigationBarTitleText": "登录"
"usingComponents": {}
}

@ -1,12 +1,15 @@
<view class="login-container">
<!-- 顶部时间显示 -->
<view class="time-display">{{currentTime}}</view>
<!--小程序图标-->
<view class="app-icon">
<image src="/images/icons/app_icon.png" class="app-image"></image>
</view>
<!-- 应用名称 -->
<view class="app-name">{{appName}}</view>
<!-- 申请使用提示 -->
<view class="permission-tip">申请使用你的微信头像、昵称信息</view>
<view class="permission-tip">申请使用你的昵称、头像</view>
<!-- 可选择提示 -->
<view class="select-tip">可选择使用不同的个人信息登录</view>
@ -29,11 +32,11 @@
<view class="add-icon">+</view>
<text class="new-text">新建头像、昵称</text>
</view>
<!-- 按钮区域 -->
<view class="button-section">
<button class="agree-btn" bindtap="handleAgree">同意</button>
<button class="cancel-btn" bindtap="handleCancel">取消</button>
</view>
</view>
<!-- 按钮区域 -->
<view class="button-section">
<button class="agree-btn" bindtap="handleAgree">同意</button>
<button class="cancel-btn" bindtap="handleCancel">取消</button>
</view>
</view>

@ -2,54 +2,67 @@
padding: 40rpx;
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.time-display {
text-align: center;
font-size: 36rpx;
color: #333;
margin-bottom: 60rpx;
font-weight: 500;
.app-image{
width: 80rpx;
height: 80rpx;
border-radius: 50%;
position: absolute;
top:20px;
left:40px;
}
.app-name {
text-align: center;
font-size: 48rpx;
position: absolute;
top:40px;
left:100px;
font-size: 30rpx;
color: #333;
margin-bottom: 80rpx;
font-weight: bold;
}
.permission-tip {
text-align: center;
position: absolute;
top:72px;
left:40px;
font-size: 36rpx;
color: #333;
margin-bottom: 20rpx;
}
.select-tip {
text-align: center;
position: absolute;
top:140px;
left:40px;
font-size: 28rpx;
color: #666;
margin-bottom: 60rpx;
}
.avatar-section {
position: absolute;
top: 180px; /* 建议使用rpx单位 */
display: flex;
flex-direction: column;
align-items: center;
gap: 40rpx;
margin-bottom: 100rpx;
width: 90%; /* 添加宽度确保布局正常 */
}
.avatar-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 40rpx;
flex-direction: row; /* 保持行排列 */
align-items: center; /* 垂直居中 */
padding: 20rpx 30rpx; /* 调整左右padding */
border-radius: 20rpx;
transition: all 0.3s;
min-width: 200rpx;
gap: 20rpx; /* 添加头像和文字之间的间距 */
}
.avatar-item.active {
@ -61,15 +74,45 @@
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-bottom: 20rpx;
/* 移除 margin-bottom因为现在是水平排列 */
background-color: #ddd;
flex-shrink: 0; /* 防止头像被压缩 */
}
.avatar-name {
font-size: 28rpx;
color: #333;
/* 添加这些属性确保文本正常显示 */
flex: 1;
white-space: nowrap; /* 防止文字换行 */
}
/* 新建头像按钮的特殊样式 */
.new-avatar {
display: flex;
flex-direction: row;
align-items: center;
gap: 20rpx;
}
.add-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: #666;
flex-shrink: 0;
}
.new-text {
font-size: 28rpx;
color: #666;
flex: 1;
}
.new-avatar {
border: 2rpx dashed #ccc;
background-color: #fafafa;
@ -93,10 +136,14 @@
color: #666;
}
/* 修改按钮区域 - 添加到头像区域内 */
.button-section {
display: flex;
flex-direction: column;
gap: 30rpx;
margin-top: 130rpx; /* 增加与新建按钮的间距 */
padding: 0 30rpx; /* 与头像项保持相同的左右padding */
}
.agree-btn, .cancel-btn {
@ -106,7 +153,8 @@
display: flex;
align-items: center;
justify-content: center;
border: none;
border: none;
width: 60px;
}
.agree-btn {

@ -0,0 +1,197 @@
// chat.js
Page({
data: {
chatUser: {
id: '',
name: '',
avatar: '',
online: true
},
userInfo: {
avatar: '/images/my-avatar.png'
},
messageList: [],
inputMessage: '',
scrollToView: '',
timeDividers: []
},
onLoad(options) {
// 从消息列表传递过来的用户ID
const userId = options.id;
// 根据用户ID获取聊天用户信息和历史消息
this.getChatUserInfo(userId);
this.getMessageHistory(userId);
},
// 获取聊天用户信息
getChatUserInfo(userId) {
// 模拟数据,实际应从服务器获取
const users = {
'1': { id: '1', name: '张三', avatar: '/images/avatar1.png', online: true },
'2': { id: '2', name: '李四', avatar: '/images/avatar2.png', online: false },
'3': { id: '3', name: '王五', avatar: '/images/avatar3.png', online: true }
};
this.setData({
chatUser: users[userId] || { id: userId, name: '糖友', avatar: '/images/default-avatar.png', online: true }
});
// 设置页面标题
wx.setNavigationBarTitle({
title: this.data.chatUser.name
});
},
// 获取历史消息
getMessageHistory(userId) {
// 模拟数据,实际应从服务器获取
const messages = [
{ id: 1, content: '你好,最近血糖控制得怎么样?', time: '10:30', isSelf: false, status: 'read' },
{ id: 2, content: '还不错,最近饮食控制得比较好', time: '10:32', isSelf: true, status: 'read' },
{ id: 3, content: '那太好了,有什么经验可以分享吗?', time: '10:33', isSelf: false, status: 'read' },
{ id: 4, content: '主要是减少了主食摄入,增加了蔬菜比例', time: '10:35', isSelf: true, status: 'read' }
];
this.setData({
messageList: messages
});
// 添加时间分隔线
this.addTimeDividers();
// 滚动到底部
this.scrollToBottom();
},
// 输入框内容变化
onInputChange(e) {
this.setData({
inputMessage: e.detail.value
});
},
// 发送消息
sendMessage() {
const message = this.data.inputMessage.trim();
if (!message) return;
// 生成新消息
const newMessage = {
id: Date.now(),
content: message,
time: this.getCurrentTime(),
isSelf: true,
status: 'sending'
};
// 添加到消息列表
this.setData({
messageList: [...this.data.messageList, newMessage],
inputMessage: ''
});
// 滚动到底部
this.scrollToBottom();
// 模拟发送到服务器
setTimeout(() => {
this.updateMessageStatus(newMessage.id, 'sent');
// 模拟对方回复
setTimeout(() => {
this.addReplyMessage();
}, 1000);
}, 500);
},
// 更新消息状态
updateMessageStatus(messageId, status) {
const messages = this.data.messageList.map(msg => {
if (msg.id === messageId) {
return { ...msg, status };
}
return msg;
});
this.setData({
messageList: messages
});
},
// 添加回复消息(模拟)
addReplyMessage() {
const replies = [
'谢谢分享,我也试试这个方法',
'最近我的血糖波动比较大,有点担心',
'你平时做什么运动来控制血糖?',
'我最近发现了一种新的低糖食谱,效果不错'
];
const randomReply = replies[Math.floor(Math.random() * replies.length)];
const replyMessage = {
id: Date.now() + 1,
content: randomReply,
time: this.getCurrentTime(),
isSelf: false,
status: 'read'
};
this.setData({
messageList: [...this.data.messageList, replyMessage]
});
// 滚动到底部
this.scrollToBottom();
},
// 滚动到底部
scrollToBottom() {
if (this.data.messageList.length > 0) {
const lastMsgId = this.data.messageList[this.data.messageList.length - 1].id;
this.setData({
scrollToView: `msg-${lastMsgId}`
});
}
},
// 添加时间分隔线
addTimeDividers() {
// 在实际应用中,可以根据消息时间添加分隔线
// 这里简单添加一个示例
this.setData({
timeDividers: ['今天']
});
},
// 获取当前时间
getCurrentTime() {
const now = new Date();
return `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`;
},
// 跳转到用户资料页面
goToUserProfile(e) {
const userId = e.currentTarget.dataset.userid;
const isSelf = e.currentTarget.dataset.isself;
if (isSelf) {
// 如果是自己,跳转到个人资料页
wx.navigateTo({
url: '/pages/profile/profile'
});
} else {
// 如果是他人,跳转到他人资料页
wx.navigateTo({
url: `/pages/otherprofile/otherprofile?userId=${userId}`
});
}
},
// 返回消息列表
goBack() {
wx.navigateBack();
}
});

@ -0,0 +1,8 @@
{
"usingComponents": {},
"navigationBarTitleText": "聊天",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}

@ -0,0 +1,58 @@
<!-- chat.wxml -->
<view class="chat-container">
<!-- 聊天消息区域 -->
<scroll-view class="chat-messages" scroll-y scroll-into-view="{{scrollToView}}" scroll-with-animation>
<view class="messages-container">
<!-- 时间分隔线 -->
<view class="time-divider" wx:for="{{timeDividers}}" wx:key="*this">
<text class="time-text">{{item}}</text>
</view>
<!-- 消息列表 -->
<!-- 在消息行的头像部分添加点击事件 -->
<view class="message-row {{item.isSelf ? 'self' : 'other'}}"
wx:for="{{messageList}}"
wx:key="id"
id="msg-{{item.id}}">
<!-- 添加 bindtap 跳转事件 -->
<image class="message-avatar"
src="{{item.isSelf ? userInfo.avatar : chatUser.avatar}}"
bindtap="goToUserProfile"
data-userid="{{item.isSelf ? userInfo.id : chatUser.id}}"
data-isself="{{item.isSelf}}"></image>
<view class="message-bubble">
<text class="message-content">{{item.content}}</text>
<view class="message-meta">
<text class="message-time">{{item.time}}</text>
<text class="message-status" wx:if="{{item.isSelf}}">
{{item.status === 'sending' ? '发送中' : (item.status === 'sent' ? '已发送' : '已读')}}
</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="input-area">
<view class="input-container">
<view class="input-center">
<input class="message-input"
value="{{inputMessage}}"
bindinput="onInputChange"
placeholder="输入消息..."
confirm-type="send"
bindconfirm="sendMessage"/>
</view>
<view class="input-right">
<image src="/images/add.png" class="input-icon"></image>
<button class="send-btn {{inputMessage ? 'active' : ''}}"
bindtap="sendMessage"
disabled="{{!inputMessage}}">
{{inputMessage ? '发送' : '语音'}}
</button>
</view>
</view>
</view>
</view>

@ -0,0 +1,527 @@
chat.wxss
/* chat.wxss - 优化输入框大小 */
page {
height: 100%;
background-color: #f8fafc;
}
.chat-container {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
}
/* 顶部导航栏 - 蓝色主题 */
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
box-shadow: 0 2px 10px rgba(24, 144, 255, 0.2);
}
.header-left {
display: flex;
align-items: center;
flex: 1;
}
.back-icon {
width: 20px;
height: 20px;
margin-right: 12px;
filter: brightness(0) invert(1);
}
.header-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
margin-right: 12px;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.header-info {
display: flex;
flex-direction: column;
}
.header-name {
font-size: 17px;
font-weight: 600;
color: #fff;
margin-bottom: 2px;
}
.header-status {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
.header-right {
display: flex;
align-items: center;
}
.header-icon {
width: 22px;
height: 22px;
margin-left: 16px;
filter: brightness(0) invert(1);
}
/* 聊天消息区域 */
.chat-messages {
flex: 1;
padding: 16px;
overflow: hidden;
background-color: #f8fafc;
}
.time-divider {
display: flex;
justify-content: center;
margin: 20px 0;
}
.time-text {
background-color: rgba(24, 144, 255, 0.1);
padding: 6px 16px;
border-radius: 16px;
font-size: 12px;
color: #1890ff;
font-weight: 500;
}
.message-row {
display: flex;
margin-bottom: 16px;
animation: fadeInUp 0.3s ease;
}
.message-row.self {
flex-direction: row-reverse;
}
.message-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
align-self: flex-end;
margin: 0 10px;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.message-bubble {
max-width: 70%;
background-color: #fff;
border-radius: 12px;
padding: 12px 16px;
position: relative;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
}
.message-row.self .message-bubble {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
border: none;
}
.message-content {
font-size: 16px;
line-height: 1.5;
color: #333;
word-break: break-word;
}
.message-row.self .message-content {
color: white;
}
.message-meta {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 6px;
}
.message-time {
font-size: 11px;
color: #999;
}
.message-row.self .message-time {
color: rgba(255, 255, 255, 0.7);
}
.message-status {
font-size: 11px;
color: #999;
margin-left: 6px;
}
.message-row.self .message-status {
color: rgba(255, 255, 255, 0.7);
}
/* 输入区域 - 优化布局防止重合和超出屏幕 */
.input-area {
background-color: #fff;
border-top: 1px solid #e8f4ff;
padding: 8px 16px 12px;
box-shadow: 0 -2px 10px rgba(24, 144, 255, 0.1);
min-height: 60px;
box-sizing: border-box;
}
.input-container {
display: flex;
align-items: flex-end;
min-height: 44px;
gap: 8px;
width: 100%;
box-sizing: border-box;
}
.input-left {
display: flex;
align-items: center;
flex-shrink: 0;
width: 50px; /* 加宽语音按钮 */
height: 44px;
justify-content: center;
}
.input-right {
display: flex;
align-items: center;
flex-shrink: 0;
width: 44px;
height: 44px;
justify-content: center;
}
.input-center {
flex: 1;
min-height: 44px;
display: flex;
align-items: center;
min-width: 0; /* 重要防止flex项目溢出 */
}
.message-input {
background-color: #f8fafc;
border-radius: 22px;
padding: 12px 18px;
font-size: 16px;
border: 1px solid #e6f7ff;
width: 100%;
min-height: 44px;
max-height: 120px;
line-height: 1.4;
box-sizing: border-box;
transition: all 0.3s ease;
flex: 1;
min-width: 0; /* 重要:防止输入框溢出 */
}
.message-input:focus {
background-color: #fff;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
/* 输入框占位符样式 */
.message-input::placeholder {
color: #999;
font-size: 16px;
}
.input-icon {
width: 28px; /* 加大语音图标 */
height: 28px;
opacity: 0.7;
transition: all 0.3s;
flex-shrink: 0;
}
.input-icon:active {
opacity: 1;
transform: scale(0.95);
}
/* 发送按钮样式优化 - 确保不超出屏幕 */
.send-btn {
background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%);
color: #999;
border: none;
border-radius: 22px;
padding: 0;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 44px;
box-sizing: border-box;
}
.send-btn.active {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
width: 44px;
min-width: 44px;
}
.send-btn:active {
transform: scale(0.95);
}
/* 语音按钮状态 */
.send-btn:not(.active) {
width: 44px;
min-width: 44px;
}
/* 语音图标 - 单独设置更大的尺寸 */
.voice-icon {
width: 28px; /* 加大语音图标 */
height: 28px;
}
/* 动画效果 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 消息气泡箭头效果 */
.message-bubble::before {
content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
border: 6px solid transparent;
}
.message-row:not(.self) .message-bubble::before {
left: -12px;
border-right-color: #fff;
}
.message-row.self .message-bubble::before {
right: -12px;
border-left-color: #1890ff;
}
/* 下拉刷新样式 */
.refresh-loading {
color: #1890ff !important;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #1890ff;
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: #096dd9;
}
/* 输入框自适应高度 */
.message-input[auto-height] {
height: auto;
}
/* 输入区域安全适配(针对有底部安全区域的手机) */
.safe-area-inset-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
/* 响应式适配 */
@media (max-width: 320px) {
.chat-messages {
padding: 12px;
}
.message-bubble {
max-width: 75%;
padding: 10px 14px;
}
.input-area {
padding: 6px 12px 10px;
}
.input-container {
min-height: 40px;
gap: 6px;
}
.input-left {
width: 46px; /* 小屏幕也保持加宽 */
height: 40px;
}
.input-right {
width: 40px;
height: 40px;
}
.message-input {
min-height: 40px;
padding: 10px 16px;
font-size: 15px;
}
.send-btn {
width: 40px;
height: 40px;
min-width: 40px;
font-size: 12px;
}
.input-icon {
width: 24px; /* 小屏幕适当缩小 */
height: 24px;
}
.voice-icon {
width: 24px;
height: 24px;
}
}
/* 大屏设备适配 */
@media (min-width: 414px) {
.input-area {
padding: 10px 20px 14px;
}
.input-container {
gap: 10px;
max-width: 100%;
}
.input-left {
width: 54px; /* 大屏幕进一步加宽 */
height: 44px;
}
.input-right {
width: 44px;
height: 44px;
}
.message-input {
min-height: 44px;
padding: 12px 18px;
}
.send-btn {
width: 44px;
height: 44px;
min-width: 44px;
}
.input-icon {
width: 30px; /* 大屏幕使用更大图标 */
height: 30px;
}
.voice-icon {
width: 30px;
height: 30px;
}
}
/* 超小屏幕特殊处理 */
@media (max-width: 280px) {
.input-area {
padding: 6px 10px 8px;
}
.input-container {
gap: 4px;
}
.input-left {
width: 42px; /* 超小屏幕保持相对宽度 */
height: 36px;
}
.input-right {
width: 36px;
height: 36px;
}
.message-input {
min-height: 36px;
padding: 8px 12px;
font-size: 14px;
}
.send-btn {
width: 36px;
height: 36px;
min-width: 36px;
font-size: 11px;
}
.input-icon {
width: 22px;
height: 22px;
}
.voice-icon {
width: 22px;
height: 22px;
}
}
/* 输入框多行文本适配 */
.message-input[style*="height"] {
transition: height 0.2s ease;
}
/* 防止布局溢出的安全样式 */
.input-container-safe {
display: grid;
grid-template-columns: 50px 1fr 44px; /* 语音按钮50px发送按钮44px */
gap: 8px;
align-items: center;
width: 100%;
box-sizing: border-box;
}
/* 计算安全宽度 */
.input-area-safe {
padding-left: calc(16px + env(safe-area-inset-left));
padding-right: calc(16px + env(safe-area-inset-right));
}

@ -1,127 +1,369 @@
Page({
data: {
planData: [],
weekDays: ["一", "二", "三", "四", "五", "六", "日"],
timeSlots: [
{ name: "空腹", value: "fasting", defaultTime: "07:00" },
{ name: "早餐后", value: "afterBreakfast", defaultTime: "09:00" },
{ name: "午餐前", value: "beforeLunch", defaultTime: "11:30" },
{ name: "午餐后", value: "afterLunch", defaultTime: "14:00" },
{ name: "晚餐前", value: "beforeDinner", defaultTime: "17:30" },
{ name: "晚餐后", value: "afterDinner", defaultTime: "20:00" },
{ name: "睡前", value: "beforeSleep", defaultTime: "22:00" },
alarmList: [
{
id: 1,
time: "07:00",
label: "空腹测量",
repeat: "daily",
enabled: true,
days: [true, true, true, true, true, true, true], // 周一到周日
},
{
id: 2,
time: "09:00",
label: "早餐后",
repeat: "weekdays",
enabled: false,
days: [true, true, true, true, true, false, false],
},
],
frequencyOptions: [
showAlarmModal: false,
editingAlarm: null,
repeatOptions: [
{ name: "每天", value: "daily" },
{ name: "工作日", value: "weekdays" },
{ name: "周末", value: "weekends" },
{ name: "自定义", value: "custom" },
],
weekDays: ["一", "二", "三", "四", "五", "六", "日"],
newAlarmTime: "07:00",
newAlarmLabel: "测量提醒",
newAlarmRepeat: "daily",
newAlarmDays: [true, true, true, true, true, true, true],
needSort: true, // 新增:是否需要排序的标志
},
onLoad: function (options) {
// 获取从monitor页面传递的数据
const eventChannel = this.getOpenerEventChannel();
if (eventChannel) {
eventChannel.on("acceptPlanData", (data) => {
this.setData({
planData: data.planData || this.initDefaultPlanData(),
weekDays: data.weekDays || this.data.weekDays,
});
});
} else {
// 如果没有传递数据,初始化默认数据
this.setData({
planData: this.initDefaultPlanData(),
});
// 从本地存储加载闹钟数据
this.loadAlarmsFromStorage();
},
// 页面显示时更新闹钟的重复文本
onShow: function () {
this.updateAlarmsRepeatText();
// 页面显示时也进行排序检查
if (this.data.needSort) {
this.sortAlarms();
}
},
// 初始化默认计划数据
initDefaultPlanData: function () {
return this.data.timeSlots.map((slot) => ({
time: slot.name,
value: slot.value,
enabled: false,
alarm: false,
alarmTime: slot.defaultTime,
frequency: "custom",
customDays: this.data.weekDays.map((day) => ({
day: day,
selected: false,
})),
// 更新所有闹钟的重复文本
updateAlarmsRepeatText: function () {
const updatedAlarmList = this.data.alarmList.map((alarm) => ({
...alarm,
repeatText: this.getRepeatText(alarm),
}));
if (
JSON.stringify(this.data.alarmList) !== JSON.stringify(updatedAlarmList)
) {
this.setData({ alarmList: updatedAlarmList });
}
},
// 切换时间段启用状态
toggleTimeSlot: function (e) {
const index = e.currentTarget.dataset.index;
const planData = [...this.data.planData];
planData[index].enabled = !planData[index].enabled;
this.setData({ planData });
// 从本地存储加载闹钟
loadAlarmsFromStorage: function () {
try {
const alarms = wx.getStorageSync("alarmList");
if (alarms) {
this.setData({ alarmList: alarms }, () => {
this.sortAlarms(); // 加载后排序
this.updateAlarmsRepeatText(); // 加载后更新文本
});
}
} catch (e) {
console.error("加载闹钟失败:", e);
}
},
// 保存闹钟到本地存储
saveAlarmsToStorage: function () {
try {
this.sortAlarms(); // 保存前排序
wx.setStorageSync("alarmList", this.data.alarmList);
} catch (e) {
console.error("保存闹钟失败:", e);
}
},
// 切换闹钟状态
// 切换闹钟开关
toggleAlarm: function (e) {
const index = e.currentTarget.dataset.index;
const planData = [...this.data.planData];
planData[index].alarm = !planData[index].alarm;
this.setData({ planData });
},
// 设置闹钟时间
setAlarmTime: function (e) {
const index = e.currentTarget.dataset.index;
const time = e.detail.value;
const planData = [...this.data.planData];
planData[index].alarmTime = time;
this.setData({ planData });
},
// 设置测量频率
setFrequency: function (e) {
const index = e.currentTarget.dataset.index;
const frequency = e.detail.value;
const planData = [...this.data.planData];
planData[index].frequency = frequency;
// 根据频率自动设置天数
if (frequency === "daily") {
planData[index].customDays.forEach((day) => (day.selected = true));
} else if (frequency === "weekdays") {
planData[index].customDays.forEach((day, i) => {
day.selected = i < 5; // 周一到周五
const alarmId = e.currentTarget.dataset.id;
const alarmList = this.data.alarmList.map((alarm) => {
if (alarm.id === alarmId) {
return { ...alarm, enabled: !alarm.enabled };
}
return alarm;
});
// 立即排序
const sortedAlarmList = this.sortAlarmsImmediately(alarmList);
this.setData({
alarmList: sortedAlarmList,
needSort: false,
});
this.saveAlarmsToStorage();
},
// 显示添加闹钟弹窗
showAddAlarmModal: function () {
this.setData({
showAlarmModal: true,
editingAlarm: null,
newAlarmTime: "07:00",
newAlarmLabel: "测量提醒",
newAlarmRepeat: "daily",
newAlarmDays: [true, true, true, true, true, true, true],
});
},
// 显示编辑闹钟弹窗
editAlarm: function (e) {
const alarmId = e.currentTarget.dataset.id;
const alarm = this.data.alarmList.find((a) => a.id === alarmId);
if (alarm) {
this.setData({
showAlarmModal: true,
editingAlarm: alarm,
newAlarmTime: alarm.time,
newAlarmLabel: alarm.label,
newAlarmRepeat: alarm.repeat,
newAlarmDays: [...alarm.days],
});
} else if (frequency === "weekends") {
planData[index].customDays.forEach((day, i) => {
day.selected = i >= 5; // 周六周日
}
},
// 隐藏闹钟弹窗
hideAlarmModal: function () {
this.setData({
showAlarmModal: false,
editingAlarm: null,
});
},
// 时间选择
onTimeChange: function (e) {
this.setData({
newAlarmTime: e.detail.value,
needSort: true, // 时间变更后需要重新排序
});
},
// 标签输入
onLabelInput: function (e) {
this.setData({
newAlarmLabel: e.detail.value,
});
},
// 重复设置选择
onRepeatChange: function (e) {
const repeat = this.data.repeatOptions[e.detail.value].value;
let newDays = [...this.data.newAlarmDays];
// 根据重复模式自动设置天数
if (repeat === "daily") {
newDays = [true, true, true, true, true, true, true];
} else if (repeat === "weekdays") {
newDays = [true, true, true, true, true, false, false];
} else if (repeat === "weekends") {
newDays = [false, false, false, false, false, true, true];
}
this.setData({
newAlarmRepeat: repeat,
newAlarmDays: newDays,
});
},
// 切换自定义天数
toggleDay: function (e) {
const dayIndex = e.currentTarget.dataset.index;
const newDays = [...this.data.newAlarmDays];
newDays[dayIndex] = !newDays[dayIndex];
this.setData({
newAlarmDays: newDays,
newAlarmRepeat: "custom", // 切换到自定义模式
});
},
// 保存闹钟
saveAlarm: function () {
const {
newAlarmTime,
newAlarmLabel,
newAlarmRepeat,
newAlarmDays,
editingAlarm,
alarmList,
} = this.data;
if (!newAlarmTime || !newAlarmLabel) {
wx.showToast({
title: "请填写完整信息",
icon: "none",
});
return;
}
// 'custom' 保持原样,让用户手动选择
this.setData({ planData });
let updatedAlarmList;
if (editingAlarm) {
// 编辑现有闹钟
updatedAlarmList = alarmList.map((alarm) =>
alarm.id === editingAlarm.id
? {
...alarm,
time: newAlarmTime,
label: newAlarmLabel,
repeat: newAlarmRepeat,
days: newAlarmDays,
}
: alarm
);
} else {
// 添加新闹钟
const newAlarm = {
id: Date.now(),
time: newAlarmTime,
label: newAlarmLabel,
repeat: newAlarmRepeat,
days: newAlarmDays,
enabled: true,
};
updatedAlarmList = [...alarmList, newAlarm];
}
// 立即排序并更新
updatedAlarmList = this.sortAlarmsImmediately(updatedAlarmList);
this.setData(
{
alarmList: updatedAlarmList,
needSort: false, // 排序完成
},
() => {
this.updateAlarmsRepeatText(); // 更新文本
this.saveAlarmsToStorage(); // 保存到存储
this.hideAlarmModal();
}
);
wx.showToast({
title: editingAlarm ? "闹钟已更新" : "闹钟已添加",
icon: "success",
});
},
// 删除闹钟
deleteAlarm: function (e) {
const alarmId = this.data.editingAlarm
? this.data.editingAlarm.id
: e.currentTarget.dataset.id;
const alarm = this.data.alarmList.find((a) => a.id === alarmId);
wx.showModal({
title: "删除闹钟",
content: `确定要删除这个闹钟吗?`,
success: (res) => {
if (res.confirm) {
const updatedAlarmList = this.data.alarmList.filter(
(a) => a.id !== alarmId
);
this.setData({ alarmList: updatedAlarmList });
this.saveAlarmsToStorage(); // 这里会自动调用 sortAlarms
this.hideAlarmModal(); // 如果在弹窗中删除,则隐藏弹窗
wx.showToast({
title: "闹钟已删除",
icon: "success",
});
}
},
});
},
// 获取重复描述文本
getRepeatText: function (alarm) {
if (!alarm) return "";
const { repeat, days } = alarm;
const dayNames = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
if (repeat === "daily") return "每天";
if (repeat === "weekdays") return "工作日";
if (repeat === "weekends") return "周末";
if (repeat === "custom") {
const selectedDays = days
.map((selected, index) => (selected ? dayNames[index] : null))
.filter(Boolean);
if (selectedDays.length === 7) return "每天";
if (selectedDays.length === 0) return "从不";
if (
selectedDays.length === 5 &&
days[0] &&
days[1] &&
days[2] &&
days[3] &&
days[4] &&
!days[5] && // 确保周六和周日未选中
!days[6]
)
return "工作日";
if (
selectedDays.length === 2 &&
days[5] &&
days[6] &&
!days[0] &&
!days[1] &&
!days[2] &&
!days[3] &&
!days[4] // 确保工作日未选中
)
return "周末";
return selectedDays.join("、");
}
return "未知";
},
// 切换自定义天数选择
toggleCustomDay: function (e) {
const { slotIndex, dayIndex } = e.currentTarget.dataset;
const planData = [...this.data.planData];
planData[slotIndex].customDays[dayIndex].selected =
!planData[slotIndex].customDays[dayIndex].selected;
// 修改现有的 sortAlarms 方法,添加立即更新
sortAlarms: function () {
if (this.data.needSort) {
const alarmList = this.sortAlarmsImmediately(this.data.alarmList);
this.setData({
alarmList,
needSort: false,
});
}
},
// 如果选择了自定义天数自动切换到custom频率
planData[slotIndex].frequency = "custom";
// 新增:立即排序方法(不依赖 setData
sortAlarmsImmediately: function (alarmList) {
return alarmList.sort((a, b) => {
// 将时间转换为分钟数进行比较
const timeToMinutes = (time) => {
if (typeof time !== "string" || !time.includes(":")) {
return 0;
}
const [hours, minutes] = time.split(":").map(Number);
return hours * 60 + minutes;
};
this.setData({ planData });
return timeToMinutes(a.time) - timeToMinutes(b.time);
});
},
// 保存计划
// 保存计划返回到monitor页面
savePlan: function () {
// 格式化数据返回给monitor页面
const formattedPlan = this.formatPlanData();
// 将闹钟数据格式化为monitor页面需要的格式
const formattedPlan = this.formatAlarmsToPlan();
// 通过事件通道返回数据
// 通过事件通道返回数据给monitor页面
const eventChannel = this.getOpenerEventChannel();
if (eventChannel) {
eventChannel.emit("planUpdated", {
@ -130,7 +372,11 @@ Page({
}
// 保存到本地存储
this.saveToStorage(formattedPlan);
try {
wx.setStorageSync("monitorPlan", formattedPlan);
} catch (e) {
console.error("保存计划失败:", e);
}
wx.showToast({
title: "计划已保存",
@ -144,51 +390,45 @@ Page({
});
},
// 格式化计划数据
formatPlanData: function () {
return this.data.planData
.filter((slot) => slot.enabled) // 只保留启用的时间段
.map((slot) => {
const days = this.data.weekDays.map((day, index) => {
const dayConfig = slot.customDays[index];
return {
day: day,
status: dayConfig && dayConfig.selected ? "empty" : "disabled",
text: dayConfig && dayConfig.selected ? "-" : "",
completed: false,
};
});
// 将闹钟格式化为计划数据
formatAlarmsToPlan: function () {
return this.data.alarmList
.filter((alarm) => alarm.enabled)
.map((alarm) => {
const days = this.data.weekDays.map((day, index) => ({
day: day,
status: alarm.days[index] ? "empty" : "disabled",
text: alarm.days[index] ? "-" : "",
completed: false,
}));
return {
time: slot.time,
alarm: slot.alarm,
alarmTime: slot.alarmTime,
time: alarm.label,
alarm: true,
alarmTime: alarm.time,
displayTime: alarm.time,
days: days,
};
});
},
// 保存到本地存储
saveToStorage: function (planData) {
try {
wx.setStorageSync("monitorPlan", planData);
} catch (e) {
console.error("保存计划失败:", e);
}
// 获取重复选项的索引
getRepeatIndex: function (repeatValue) {
const index = this.data.repeatOptions.findIndex(
(option) => option.value === repeatValue
);
return index >= 0 ? index : 0;
},
// 重置计划
resetPlan: function () {
wx.showModal({
title: "重置计划",
content: "确定要重置所有设置吗?",
success: (res) => {
if (res.confirm) {
this.setData({
planData: this.initDefaultPlanData(),
});
}
},
});
// 获取重复选项的名称
getRepeatOptionName: function (repeatValue) {
const option = this.data.repeatOptions.find(
(option) => option.value === repeatValue
);
return option ? option.name : "每天";
},
stopPropagation: function () {
// 空函数,用于阻止事件冒泡
},
});

@ -1,5 +1,8 @@
{
"usingComponents": {},
"navigationBarTitleText": "调整监测计划",
"enablePullDownRefresh": false
"navigationBarTitleText": "测量闹钟",
"enablePullDownRefresh": false,
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#2196F3",
"backgroundColor": "#f5f5f5"
}

@ -1,87 +1,132 @@
<view class="container">
<!-- 计划说明 -->
<view class="intro-card card">
<view class="intro-title">调整您的监测计划</view>
<text class="intro-text">
请选择您需要监测的时间段,并设置提醒时间。系统会根据您的选择生成每周的监测计划。
</text>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{alarmList.length === 0}}">
<image src="/images/icons/clock.svg" class="empty-icon" mode="aspectFit"></image>
<view class="empty-text">暂无闹钟</view>
<view class="empty-desc">点击下方+号添加测量提醒</view>
</view>
<!-- 时间段设置 -->
<view class="time-slots card">
<view class="section-title">监测时间段设置</view>
<view class="time-slot-item" wx:for="{{planData}}" wx:key="value" wx:for-index="index">
<view class="slot-header">
<view class="slot-info">
<view class="slot-name">{{item.time}}</view>
<view class="slot-status {{item.enabled ? 'enabled' : 'disabled'}}">
{{item.enabled ? '已启用' : '未启用'}}
</view>
<!-- 闹钟列表 -->
<view class="alarm-list" wx:else>
<view
class="alarm-item"
wx:for="{{alarmList}}"
wx:key="id"
bindtap="editAlarm"
data-id="{{item.id}}"
>
<view class="alarm-content">
<view class="alarm-time">{{item.time}}</view>
<view class="alarm-info">
<view class="alarm-label">{{item.label}}</view>
<view class="alarm-repeat">{{item.repeatText}}</view>
</view>
<switch checked="{{item.enabled}}" bindchange="toggleTimeSlot" data-index="{{index}}"/>
</view>
<view class="alarm-actions" catchtap="stopPropagation">
<switch checked="{{item.enabled}}" bindchange="toggleAlarm" data-id="{{item.id}}" />
</view>
</view>
</view>
<!-- 启用时的详细设置 -->
<view class="slot-details" wx:if="{{item.enabled}}">
<!-- 闹钟设置 -->
<view class="setting-row">
<view class="setting-label">闹钟提醒</view>
<switch checked="{{item.alarm}}" bindchange="toggleAlarm" data-index="{{index}}"/>
</view>
<view class="setting-row" wx:if="{{item.alarm}}">
<view class="setting-label">提醒时间</view>
<picker mode="time" value="{{item.alarmTime}}" bindchange="setAlarmTime" data-index="{{index}}">
<view class="time-picker">{{item.alarmTime}}</view>
<!-- 悬浮添加按钮 -->
<view class="fab" bindtap="showAddAlarmModal">
<image src="/images/icons/plus.svg" class="fab-icon" mode="aspectFit"></image>
</view>
<!-- 添加/编辑闹钟弹窗 -->
<view class="modal-overlay" wx:if="{{showAlarmModal}}" catchtap="hideAlarmModal">
<view class="alarm-modal" catchtap="stopPropagation">
<view class="modal-header">
<view class="modal-title">{{editingAlarm ? '编辑闹钟' : '添加闹钟'}}</view>
<view class="close-btn" bindtap="hideAlarmModal">×</view>
</view>
<view class="modal-body">
<!-- 时间选择 -->
<view class="time-section">
<picker mode="time" value="{{newAlarmTime}}" bindchange="onTimeChange">
<view class="time-display">{{newAlarmTime}}</view>
</picker>
</view>
<!-- 频率设置 -->
<view class="setting-row">
<view class="setting-label">测量频率</view>
<picker range="{{frequencyOptions}}" range-key="name" value="{{getFrequencyIndex(item.frequency)}}" bindchange="setFrequency" data-index="{{index}}">
<view class="frequency-picker">
{{frequencyOptions[getFrequencyIndex(item.frequency)].name}}
<!-- 标签输入 -->
<view class="input-group">
<view class="input-label">标签</view>
<input
class="input-field"
placeholder="请输入标签"
value="{{newAlarmLabel}}"
bindinput="onLabelInput"
/>
</view>
<!-- 重复设置 -->
<view class="input-group">
<view class="input-label">重复</view>
<picker
range="{{repeatOptions}}"
range-key="name"
value="{{utils.getRepeatIndex(newAlarmRepeat, repeatOptions)}}"
bindchange="onRepeatChange"
>
<view class="picker-display">
{{utils.getRepeatOptionName(newAlarmRepeat, repeatOptions)}}
</view>
</picker>
</view>
<!-- 自定义天数选择 -->
<view class="custom-days" wx:if="{{item.frequency === 'custom'}}">
<view class="days-label">选择测量日期:</view>
<!-- 自定义天数 -->
<view class="days-section" wx:if="{{newAlarmRepeat === 'custom'}}">
<view class="days-label">选择重复日期</view>
<view class="days-grid">
<view
class="day-item {{day.selected ? 'selected' : ''}}"
wx:for="{{item.customDays}}"
wx:key="day"
wx:for-index="dayIndex"
bindtap="toggleCustomDay"
data-slot-index="{{index}}"
data-day-index="{{dayIndex}}"
class="day-item {{newAlarmDays[index] ? 'selected' : ''}}"
wx:for="{{weekDays}}"
wx:key="*this"
bindtap="toggleDay"
data-index="{{index}}"
>
{{day.day}}
{{item}}
</view>
</view>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn-container" wx:if="{{editingAlarm}}">
<view class="modal-btn delete" bindtap="deleteAlarm" data-id="{{editingAlarm.id}}">删除</view>
</view>
<view class="modal-btn cancel" bindtap="hideAlarmModal">取消</view>
<view class="modal-btn confirm" bindtap="saveAlarm">保存</view>
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="btn secondary" bindtap="resetPlan">重置</view>
<view class="btn primary" bindtap="savePlan">保存计划</view>
</view>
<!-- WXS 模块 -->
<wxs module="utils">
var getRepeatIndex = function(repeatValue, repeatOptions) {
if (!repeatOptions) return 0;
for (var i = 0; i < repeatOptions.length; i++) {
if (repeatOptions[i].value === repeatValue) {
return i;
}
}
return 0;
}
<!-- 提示信息 -->
<view class="tips-card card">
<view class="tips-title">设置说明:</view>
<text class="tips-content">
• 开启时间段后才会在监测计划中显示\n
• 闹钟提醒会在设定时间推送测量通知\n
• 建议根据医生建议设置合适的测量频率\n
• 保存后可在监测计划页面查看效果
</text>
</view>
</view>
var getRepeatOptionName = function(repeatValue, repeatOptions) {
if (!repeatOptions) return '每天';
for (var i = 0; i < repeatOptions.length; i++) {
if (repeatOptions[i].value === repeatValue) {
return repeatOptions[i].name;
}
}
return '每天';
}
module.exports = {
getRepeatIndex: getRepeatIndex,
getRepeatOptionName: getRepeatOptionName,
}
</wxs>

@ -1,180 +1,322 @@
.container {
padding: 20rpx;
padding-bottom: 100rpx;
padding-bottom: 180rpx; /* 增加底部内边距以容纳保存按钮 */
min-height: 100vh;
background: #f5f5f5;
box-sizing: border-box;
}
.intro-card {
padding: 30rpx;
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
text-align: center;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.intro-title {
.empty-text {
font-size: 32rpx;
font-weight: bold;
color: #333;
color: #999;
margin-bottom: 15rpx;
}
.intro-text {
.empty-desc {
font-size: 26rpx;
color: #666;
line-height: 1.6;
color: #ccc;
}
.time-slots {
/* 闹钟列表 */
.alarm-list {
margin-bottom: 30rpx;
}
.section-title {
.alarm-item {
display: flex;
align-items: center;
background: white;
padding: 30rpx;
margin-bottom: 20rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
}
.alarm-item:active {
transform: scale(0.98);
}
.alarm-content {
flex: 1;
display: flex;
align-items: center;
}
.alarm-time {
font-size: 64rpx;
font-weight: 300;
color: #333;
margin-right: 30rpx;
font-variant-numeric: tabular-nums;
}
.alarm-info {
display: flex;
flex-direction: column;
}
.alarm-label {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 8rpx;
}
.time-slot-item {
padding: 30rpx 0;
border-bottom: 1rpx solid #f8f8f8;
.alarm-repeat {
font-size: 26rpx;
color: #666;
}
.time-slot-item:last-child {
border-bottom: none;
.alarm-actions {
display: flex;
align-items: center;
gap: 20rpx;
}
.slot-header {
/* 悬浮按钮 */
.fab {
position: fixed;
bottom: 140rpx;
right: 40rpx;
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: transparent; /* 移除绿色背景 */
display: flex;
justify-content: space-between;
align-items: center;
justify-content: center;
box-shadow: none; /* 移除阴影 */
z-index: 1000;
}
.slot-info {
flex: 1;
.fab:active {
transform: scale(0.95);
}
.fab-icon {
width: 80rpx; /* 放大图标 */
height: 80rpx;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3)); /* 添加阴影效果 */
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 40rpx;
}
.slot-name {
font-size: 30rpx;
.alarm-modal {
background: white;
width: 100%;
border-radius: 20rpx;
overflow: hidden;
max-width: 600rpx;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 30rpx 20rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.slot-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
.close-btn {
font-size: 50rpx;
color: #999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.slot-status.enabled {
background: #e8f5e8;
color: #4caf50;
.modal-body {
padding: 20rpx 30rpx 40rpx;
}
.slot-status.disabled {
background: #f5f5f5;
color: #999;
/* 时间选择 */
.time-section {
text-align: center;
margin-bottom: 30rpx;
}
.slot-details {
margin-top: 25rpx;
padding-top: 25rpx;
border-top: 1rpx solid #f0f0f0;
.time-display {
font-size: 72rpx;
font-weight: 300;
color: #333;
font-variant-numeric: tabular-nums;
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
}
.setting-row {
/* 输入组 */
.input-group {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25rpx;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.setting-label {
font-size: 28rpx;
.input-label {
font-size: 32rpx;
color: #333;
}
.time-picker,
.frequency-picker {
padding: 15rpx 25rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
.input-field {
font-size: 32rpx;
color: #333;
text-align: right;
flex: 1;
margin-left: 20rpx;
}
.picker-display {
font-size: 32rpx;
color: #333;
}
.custom-days {
margin-top: 20rpx;
/* 天数选择 */
.days-section {
margin-top: 30rpx;
}
.days-label {
font-size: 26rpx;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
margin-bottom: 25rpx;
}
.days-grid {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
justify-content: space-between;
}
.day-item {
width: 70rpx;
height: 70rpx;
border-radius: 35rpx;
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
color: #666;
font-size: 26rpx;
font-size: 28rpx;
transition: background-color 0.2s, color 0.2s;
}
.day-item.selected {
background: #4caf50;
color: #fff;
color: white;
}
.action-buttons {
/* 弹窗底部按钮 */
.modal-footer {
display: flex;
gap: 20rpx;
margin: 40rpx 0;
border-top: 1rpx solid #f0f0f0;
align-items: center;
}
.btn {
flex: 1;
padding: 25rpx;
.modal-btn-container {
flex-basis: 25%;
}
.modal-btn {
text-align: center;
border-radius: 10rpx;
font-size: 30rpx;
padding: 30rpx;
font-size: 32rpx;
font-weight: bold;
transition: background-color 0.2s;
}
.btn.primary {
background: #4caf50;
color: #fff;
.modal-btn:active {
background-color: #f0f0f0;
}
.btn.secondary {
background: #f5f5f5;
.modal-btn.delete {
color: #e53935;
font-weight: normal;
}
.modal-btn.cancel {
color: #666;
flex-basis: 40%;
}
.tips-card {
padding: 30rpx;
.modal-btn.confirm {
color: #4caf50;
flex-basis: 60%;
border-left: 1rpx solid #f0f0f0;
}
.tips-title {
font-size: 28rpx;
/* 保存按钮 */
.save-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
z-index: 1001;
}
.save-btn {
background: #4caf50;
color: white;
text-align: center;
padding: 25rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
transition: background-color 0.2s;
}
.tips-content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
white-space: pre-line;
.save-btn:active {
background-color: #45a049;
}
/* 阻止事件冒泡 */
.stop-propagation {
/* 空样式用于catch事件 */
}

@ -1,6 +1,7 @@
Page({
data: {
currentTab: 0,
isEditingPlan: false, // 添加编辑状态控制
weekDays: ["一", "二", "三", "四", "五", "六", "日"],
devices: [
{
@ -8,7 +9,7 @@ Page({
name: "血糖仪",
description: "精准测量,无需试纸",
icon: "/images/icons/device.png",
connected: true,
connected: false,
},
{
id: 2,
@ -18,39 +19,191 @@ Page({
connected: false,
},
],
// 修改 planData 初始化数据,将 text 从 "-" 改为 ""
planData: [
{
time: "凌晨",
alarm: false,
alarmTime: "",
displayTime: "03:00",
days: [
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "空腹",
alarm: true,
alarmTime: "07:00",
alarm: false,
alarmTime: "",
displayTime: "06:30",
days: [
{ day: "一", status: "empty", text: "-", completed: false },
{ day: "二", status: "empty", text: "-", completed: false },
{ day: "三", status: "empty", text: "-", completed: false },
{ day: "四", status: "empty", text: "-", completed: false },
{ day: "五", status: "empty", text: "-", completed: false },
{ day: "六", status: "empty", text: "-", completed: false },
{ day: "日", status: "empty", text: "-", completed: false },
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "早餐后",
alarm: false,
alarmTime: "",
displayTime: "09:00",
days: [
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "午餐前",
alarm: false,
alarmTime: "",
displayTime: "11:30",
days: [
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "午餐后",
alarm: false,
alarmTime: "",
displayTime: "14:00",
days: [
{ day: "一", status: "checked", text: "✓", completed: true },
{ day: "二", status: "checked", text: "✓", completed: true },
{ day: "三", status: "checked", text: "✓", completed: true },
{ day: "四", status: "empty", text: "-", completed: false },
{ day: "五", status: "empty", text: "-", completed: false },
{ day: "六", status: "empty", text: "-", completed: false },
{ day: "日", status: "empty", text: "-", completed: false },
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "晚餐前",
alarm: false,
alarmTime: "",
displayTime: "17:30",
days: [
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "晚餐后",
alarm: false,
alarmTime: "",
displayTime: "20:00",
days: [
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
{
time: "睡前",
alarm: false,
alarmTime: "",
displayTime: "22:00",
days: [
{ day: "一", status: "empty", text: "", completed: false },
{ day: "二", status: "empty", text: "", completed: false },
{ day: "三", status: "empty", text: "", completed: false },
{ day: "四", status: "empty", text: "", completed: false },
{ day: "五", status: "empty", text: "", completed: false },
{ day: "六", status: "empty", text: "", completed: false },
{ day: "日", status: "empty", text: "", completed: false },
],
},
// 其他时间段数据...
],
hasBluetoothPermission: false,
isBluetoothEnabled: false,
isScanning: false,
discoveredDevices: [],
showTimePicker: false, // 添加这个控制变量
showPlanDescModal: false, // 添加这个字段
showBluetoothList: false, // 控制蓝牙设备列表浮层显示
currentEditTimeIndex: null,
inputHour: "00",
inputMinute: "00",
planDescList: [
{
title: "空腹",
desc: "隔夜空腹(至少8小时,未进水外的任何物)后,早餐前来测量血糖值",
},
{
title: "早餐后",
desc: "从吃第一口早餐开始计时,2小时后采血测量的血糖值",
},
{
title: "午餐前",
desc: "午餐前采血测量的血糖值",
},
{
title: "午餐后",
desc: "从吃第一口午餐开始计时,2小时后采血测量的血糖值",
},
{
title: "晚餐前",
desc: "晚餐前采血测量的血糖值",
},
{
title: "晚餐后",
desc: "从吃第一口晚餐开始计时,2小时后采血测量的血糖值",
},
{
title: "睡前",
desc: "睡觉前采血测量的血糖值,一般建议晚上22:00前入睡",
},
{
title: "夜间",
desc: "凌晨(一般指凌晨2点-3点)时采血测量的血糖值",
},
{
title: "早餐配对",
desc: "饮食对血糖影响很大,做好饮食配对监测,可以帮助我们根据血糖变化调整饮食,找到适合自己的饮食方式。",
},
{
title: "午餐配对",
desc: "饮食对血糖影响很大,做好饮食配对监测,可以帮助我们根据血糖变化调整饮食,找到适合自己的饮食方式。",
},
{
title: "晚餐配对",
desc: "饮食对血糖影响很大,做好饮食配对监测,可以帮助我们根据血糖变化调整饮食,找到适合自己的饮食方式。",
},
],
},
// 阻止事件冒泡
stopPropagation: function () {},
onLoad: function (options) {
// 监听调整计划页面的返回数据
const eventChannel = this.getOpenerEventChannel();
@ -62,6 +215,41 @@ Page({
this.savePlanToStorage();
});
}
this.checkBluetoothStatus();
},
onShow: function () {
// 每次页面显示时检查蓝牙状态
this.checkBluetoothStatus();
},
// 检查蓝牙状态
checkBluetoothStatus: function () {
// 检查蓝牙适配器状态
wx.getBluetoothAdapterState({
success: (res) => {
this.setData({
isBluetoothEnabled: res.available,
hasBluetoothPermission: true,
});
},
fail: (err) => {
console.error("检查蓝牙状态失败:", err);
this.setData({
isBluetoothEnabled: false,
hasBluetoothPermission: false,
});
// 提示用户开启蓝牙
if (err.errCode === 10001) {
wx.showModal({
title: "蓝牙未开启",
content: "请开启手机蓝牙功能以连接设备",
showCancel: false,
});
}
},
});
},
switchTab: function (e) {
@ -69,53 +257,259 @@ Page({
this.setData({ currentTab: tab });
},
// 增强扫描设备功能
scanDevice: function () {
wx.showActionSheet({
itemList: ["扫码连接", "蓝牙连接"],
// 扫描二维码添加设备
scanQRCode: function () {
if (!this.data.isBluetoothEnabled) {
this.showBluetoothGuide();
return;
}
console.log("开始扫描设备二维码...");
wx.showLoading({
title: "准备扫描...",
});
wx.scanCode({
onlyFromCamera: true,
scanType: ["qrCode"],
success: (res) => {
if (res.tapIndex === 0) {
// 扫码连接
wx.scanCode({
success: (res) => {
console.log("扫描结果:", res);
this.addScannedDevice(res.result);
},
fail: (err) => {
wx.showToast({
title: "扫描失败",
icon: "none",
});
},
wx.hideLoading();
console.log("二维码扫描结果:", res);
if (res.result) {
this.processQRCodeResult(res.result);
} else {
wx.showToast({
title: "未识别到有效二维码",
icon: "none",
});
}
},
fail: (err) => {
wx.hideLoading();
console.error("扫描失败:", err);
if (err.errMsg.indexOf("auth deny") !== -1) {
this.showCameraPermissionGuide();
} else {
wx.showToast({
title: "扫描失败,请重试",
icon: "none",
});
}
},
});
},
// 处理二维码扫描结果
processQRCodeResult: function (qrResult) {
try {
// 尝试解析二维码内容为设备信息
let deviceInfo;
try {
deviceInfo = JSON.parse(qrResult);
} catch (e) {
// 如果不是JSON格式创建基础设备信息
deviceInfo = {
name: qrResult,
type: "unknown",
description: "通过二维码扫描添加",
};
}
// 验证设备信息
if (!deviceInfo.name) {
wx.showToast({
title: "二维码内容无效",
icon: "none",
});
return;
}
// 显示设备确认对话框
wx.showModal({
title: "发现设备",
content: `是否添加设备 "${deviceInfo.name}"`,
confirmText: "添加",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
this.addDeviceFromQRCode(deviceInfo);
}
},
});
} catch (error) {
console.error("处理二维码结果失败:", error);
wx.showToast({
title: "处理二维码失败",
icon: "none",
});
}
},
// 从二维码添加设备
addDeviceFromQRCode: function (deviceInfo) {
const newDevice = {
id: "qr_" + Date.now(),
name: deviceInfo.name || "扫描设备",
description: deviceInfo.description || "通过二维码添加",
type: deviceInfo.type || "general",
icon: deviceInfo.icon || "/images/icons/device.png",
connected: false,
connectionMethod: "qr_code",
addedTime: new Date().toLocaleString(),
};
this.addDeviceToList(newDevice);
},
// 扫描蓝牙设备
scanBluetoothDevice: function () {
if (!this.data.isBluetoothEnabled) {
this.showBluetoothGuide();
return;
}
if (this.data.isScanning) {
wx.showToast({
title: "正在扫描中...",
icon: "none",
});
return;
}
console.log("开始蓝牙设备扫描...");
this.setData({
isScanning: true,
discoveredDevices: [], // 清空之前发现的设备
});
wx.showLoading({
title: "搜索蓝牙设备...",
});
// 初始化蓝牙模块
wx.openBluetoothAdapter({
success: (res) => {
console.log("蓝牙适配器打开成功");
this.startBluetoothDiscovery();
},
fail: (err) => {
wx.hideLoading();
this.setData({ isScanning: false });
console.error("打开蓝牙适配器失败:", err);
if (err.errCode === 10001) {
this.showBluetoothGuide();
} else {
wx.showToast({
title: "蓝牙初始化失败",
icon: "none",
});
} else if (res.tapIndex === 1) {
// 蓝牙连接
this.connectBluetoothDevice();
}
},
});
},
// 蓝牙连接设备
connectBluetoothDevice: function () {
// 开始蓝牙设备发现
startBluetoothDiscovery: function () {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: (res) => {
console.log("开始蓝牙设备发现");
// 监听发现新设备事件
wx.onBluetoothDeviceFound((devices) => {
console.log("发现蓝牙设备:", devices);
this.processDiscoveredDevices(devices.devices);
});
// 3秒后停止搜索从10秒改为3秒
setTimeout(() => {
this.stopBluetoothDiscovery();
// 扫描结束后显示浮层
if (this.data.discoveredDevices.length === 0) {
wx.showToast({
title: "未发现蓝牙设备",
icon: "none",
});
} else {
this.setData({
showBluetoothList: true,
});
}
}, 3000); // 改为3000毫秒3秒
wx.hideLoading();
wx.showToast({
title: "扫描中,请稍候...",
icon: "none",
duration: 3000,
});
},
fail: (err) => {
wx.hideLoading();
this.setData({ isScanning: false });
console.error("蓝牙设备发现失败:", err);
wx.showToast({
title: "设备搜索失败",
icon: "none",
});
},
});
},
// 处理发现的设备
processDiscoveredDevices: function (devices) {
const validDevices = devices.filter(
(device) => device.name && device.name.length > 0
);
if (validDevices.length > 0) {
// 去重处理,避免重复设备
const existingDeviceIds = this.data.discoveredDevices.map(
(device) => device.deviceId
);
const newDevices = validDevices.filter(
(device) => !existingDeviceIds.includes(device.deviceId)
);
if (newDevices.length > 0) {
this.setData({
discoveredDevices: [
...this.data.discoveredDevices,
...newDevices,
].slice(0, 10), // 限制最多显示10个设备
});
}
}
},
// 连接到蓝牙设备
connectToBluetoothDevice: function (e) {
const device = e.currentTarget.dataset.device;
wx.showLoading({
title: "搜索设备中...",
title: `连接${device.name}...`,
});
// 模拟蓝牙设备搜索和连接过程
// 模拟连接过程实际开发中需要调用具体的蓝牙连接API
setTimeout(() => {
wx.hideLoading();
const newDevice = {
id: Date.now(),
name: "蓝牙血糖仪",
description: "通过蓝牙连接",
id: "bt_" + device.deviceId,
name: device.name || "蓝牙设备",
description: `蓝牙设备 ${device.deviceId}`,
type: "bluetooth",
icon: "/images/icons/bluetooth-device.png",
connected: true,
connectionMethod: "bluetooth",
deviceId: device.deviceId,
lastConnected: new Date().toLocaleString(),
};
this.setData({
devices: [...this.data.devices, newDevice],
});
this.addDeviceToList(newDevice);
this.hideBluetoothList(); // 连接成功后关闭浮层
wx.showToast({
title: "设备连接成功",
@ -124,25 +518,143 @@ Page({
}, 2000);
},
manualAddDevice: function () {
wx.navigateTo({
url: "/pages/monitor/add-device/add-device",
// 停止蓝牙设备发现
stopBluetoothDiscovery: function () {
wx.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log("停止蓝牙设备发现");
this.setData({ isScanning: false });
},
fail: (err) => {
console.error("停止蓝牙设备发现失败:", err);
this.setData({ isScanning: false });
},
});
},
addScannedDevice: function (deviceInfo) {
// 添加设备到列表
addDeviceToList: function (newDevice) {
// 检查是否已存在相同设备
const exists = this.data.devices.some(
(device) =>
device.id === newDevice.id ||
(device.connectionMethod === "bluetooth" &&
device.deviceId === newDevice.deviceId)
);
if (exists) {
wx.showToast({
title: "设备已存在",
icon: "none",
});
return;
}
this.setData(
{
devices: [...this.data.devices, newDevice],
},
() => {
this.sortDevices();
wx.showToast({
title: "设备添加成功",
icon: "success",
});
}
);
},
// 显示摄像头权限引导
showCameraPermissionGuide: function () {
wx.showModal({
title: "需要摄像头权限",
content: "扫码添加设备需要使用摄像头,请在设置中允许摄像头权限",
confirmText: "去设置",
success: (res) => {
if (res.confirm) {
wx.openSetting();
}
},
});
},
// 显示蓝牙引导提示
showBluetoothGuide: function () {
wx.showModal({
title: "蓝牙未准备好",
content:
"请确保:\n1. 已授予小程序蓝牙权限\n2. 手机蓝牙功能已开启\n3. 设备处于可被发现状态",
confirmText: "去设置",
success: (res) => {
if (res.confirm) {
// 打开蓝牙设置
wx.openBluetoothAdapter({
success: () => {
this.checkBluetoothStatus();
},
fail: () => {
wx.showToast({
title: "请手动开启蓝牙",
icon: "none",
});
},
});
}
},
});
},
addScannedDevice: function (deviceInfoString) {
// 模拟解析deviceInfo字符串
let deviceInfo;
try {
// 假设deviceInfoString是JSON字符串
deviceInfo = JSON.parse(deviceInfoString);
} catch (e) {
// 如果不是JSON则当成一个简单的name
deviceInfo = { name: deviceInfoString };
}
// 验证设备信息
if (!deviceInfo || !deviceInfo.name) {
wx.showToast({
title: "设备信息不完整",
icon: "none",
});
return;
}
// 检查是否已存在相同设备
const exists = this.data.devices.some(
(device) => device.name === deviceInfo.name
);
if (exists) {
wx.showToast({
title: "设备已存在",
icon: "none",
});
return;
}
// 处理扫描到的设备信息
const newDevice = {
id: Date.now(),
name: "新设备",
description: "通过扫描添加",
icon: "/images/icons/device.png",
name: deviceInfo.name || "新设备",
description: deviceInfo.description || "通过扫描添加",
icon: deviceInfo.icon || "/images/icons/device.png",
connected: false,
lastConnected: null,
};
this.setData({
devices: [...this.data.devices, newDevice],
});
this.setData(
{
devices: [...this.data.devices, newDevice],
},
() => {
this.sortDevices();
}
);
wx.showToast({
title: "设备添加成功",
@ -182,15 +694,21 @@ Page({
const deviceId = e.currentTarget.dataset.id;
const devices = this.data.devices.map((device) => {
if (device.id === deviceId) {
const isConnected = !device.connected;
return {
...device,
connected: !device.connected,
connected: isConnected,
lastConnected: isConnected
? new Date().toLocaleString()
: device.lastConnected,
};
}
return device;
});
this.setData({ devices });
this.setData({ devices }, () => {
this.sortDevices();
});
wx.showToast({
title: devices.find((d) => d.id === deviceId).connected
@ -200,36 +718,31 @@ Page({
});
},
showPlanDesc: function () {
wx.showModal({
title: "餐点说明",
content: "这里显示各个餐点的详细说明和测量要求...",
showCancel: false,
// 按连接状态排序
sortDevices: function () {
const sortedDevices = this.data.devices.sort((a, b) => {
return b.connected - a.connected;
});
this.setData({ devices: sortedDevices });
},
// 增强调整计划功能
adjustPlan: function () {
wx.navigateTo({
url: "/pages/monitor/adjust-plan/adjust-plan",
success: (res) => {
// 传递当前计划数据到调整页面
res.eventChannel.emit("acceptPlanData", {
planData: this.data.planData,
weekDays: this.data.weekDays,
});
},
});
showPlanDesc: function () {
this.setData({ showPlanDescModal: true });
},
// 保存计划到本地存储
hidePlanDescModal: function () {
this.setData({ showPlanDescModal: false });
},
// 保存计划到本地存储 - 移除提示
savePlanToStorage: function () {
try {
wx.setStorageSync("monitorPlan", this.data.planData);
wx.showToast({
title: "计划已保存",
icon: "success",
});
// 移除保存成功的提示
// wx.showToast({
// title: "计划已保存",
// icon: "success",
// });
} catch (e) {
console.error("保存计划失败:", e);
}
@ -260,7 +773,173 @@ Page({
});
},
// 切换测量状态
getFrequencyIndex: function (frequency) {
const frequencyOptions = [
{ name: "每天", value: "daily" },
{ name: "工作日", value: "weekdays" },
{ name: "周末", value: "weekends" },
{ name: "自定义", value: "custom" },
];
return frequencyOptions.findIndex((item) => item.value === frequency);
},
// 显示时间输入弹窗
showTimePicker: function (e) {
const index = e.currentTarget.dataset.index;
const currentTime = this.data.planData[index].displayTime;
const [hour, minute] = currentTime.split(":");
this.setData({
showTimePicker: true,
currentEditTimeIndex: index,
inputHour: hour,
inputMinute: minute,
});
},
// 小时输入处理
onHourInput: function (e) {
let hour = e.detail.value;
// 限制小时范围 00-23
if (hour > 23) {
hour = "23";
} else if (hour < 0) {
hour = "00";
}
this.setData({
inputHour: hour,
});
},
// 分钟输入处理
onMinuteInput: function (e) {
let minute = e.detail.value;
// 限制分钟范围 00-59
if (minute > 59) {
minute = "59";
} else if (minute < 0) {
minute = "00";
}
this.setData({
inputMinute: minute,
});
},
// 确认时间输入
confirmTimeInput: function () {
const { currentEditTimeIndex, inputHour, inputMinute, planData } =
this.data;
if (currentEditTimeIndex === null) {
this.hideTimePicker();
return;
}
// 验证输入
if (!inputHour || !inputMinute) {
wx.showToast({
title: "请输入完整时间",
icon: "none",
});
return;
}
const hour = parseInt(inputHour);
const minute = parseInt(inputMinute);
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
wx.showToast({
title: "时间格式不正确",
icon: "none",
});
return;
}
const newTime = `${inputHour.padStart(2, "0")}:${inputMinute.padStart(
2,
"0"
)}`;
// 更新数据
const updatedPlanData = [...planData];
updatedPlanData[currentEditTimeIndex].displayTime = newTime;
this.setData({
planData: updatedPlanData,
showTimePicker: false,
currentEditTimeIndex: null,
});
this.savePlanToStorage();
wx.showToast({
title: `已设置为 ${newTime}`,
icon: "success",
duration: 1500,
});
},
// 隐藏时间选择器
hideTimePicker: function () {
this.setData({
showTimePicker: false,
currentEditTimeIndex: null,
});
},
// 切换编辑模式
toggleEditMode: function () {
const isEditing = !this.data.isEditingPlan;
this.setData({
isEditingPlan: isEditing,
});
if (isEditing) {
wx.showToast({
title: "进入编辑模式",
icon: "none",
});
} else {
// 退出编辑模式时保存
this.savePlanToStorage();
wx.showToast({
title: "计划已保存",
icon: "success",
});
}
},
// 调整计划 - 跳转到 adjust-plan 页面
adjustPlan: function () {
wx.navigateTo({
url: "/pages/monitor/adjust-plan/adjust-plan",
success: (res) => {
// 通过事件通道传递当前计划数据
res.eventChannel.emit("sendPlanData", {
planData: this.data.planData,
});
},
});
},
// 显示蓝牙设备列表浮层
showBluetoothList: function () {
this.setData({
showBluetoothList: true,
});
},
// 隐藏蓝牙设备列表浮层
hideBluetoothList: function () {
this.setData({
showBluetoothList: false,
isScanning: false,
});
// 停止蓝牙扫描
this.stopBluetoothDiscovery();
},
// 修改 toggleMeasurement 函数,将未完成状态的 text 设为 ""
toggleMeasurement: function (e) {
const { timeIndex, dayIndex } = e.currentTarget.dataset;
const planData = [...this.data.planData];
@ -268,19 +947,8 @@ Page({
cell.completed = !cell.completed;
cell.status = cell.completed ? "checked" : "empty";
cell.text = cell.completed ? "✓" : "-";
cell.text = cell.completed ? "✓" : ""; // 这里将 "-" 改为 ""
this.setData({ planData });
this.savePlanToStorage();
},
getFrequencyIndex: function (frequency) {
const frequencyOptions = [
{ name: "每天", value: "daily" },
{ name: "工作日", value: "weekdays" },
{ name: "周末", value: "weekends" },
{ name: "自定义", value: "custom" },
];
return frequencyOptions.findIndex((item) => item.value === frequency);
},
});

@ -1,5 +1,7 @@
{
"usingComponents": {},
"navigationBarTitleText": "监测计划",
"enablePullDownRefresh": false
}
"enablePullDownRefresh": false,
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#2196f3"
}

@ -9,58 +9,69 @@
<view class="tab-content" wx:if="{{currentTab === 0}}">
<!-- 添加设备 -->
<view class="add-device card">
<view class="add-title">扫一扫添加设备</view>
<view class="add-title">添加设备</view>
<view class="add-actions">
<view class="add-action" bindtap="scanDevice">
<view class="add-action" bindtap="scanQRCode">
<image src="/images/icons/scan.png" class="action-icon"></image>
<text>扫一扫</text>
<text>扫二维码</text>
<text class="action-desc">扫描设备二维码快速配对</text>
</view>
<view class="add-action" bindtap="manualAddDevice">
<image src="/images/icons/add.png" class="action-icon"></image>
<text>添加设备</text>
<view class="add-action" bindtap="scanBluetoothDevice">
<image src="/images/icons/bluetooth.svg" class="action-icon"></image>
<text>蓝牙扫描</text>
<text class="action-desc">搜索附近蓝牙设备</text>
</view>
</view>
<text class="add-tip">可添加多个设备</text>
<text class="add-tip">需要蓝牙权限并开启蓝牙功能</text>
</view>
<!-- 我的设备 -->
<view class="my-devices card">
<view class="section-title">我的设备</view>
<view class="device-list">
<view class="device-item" wx:for="{{devices}}" wx:key="id">
<image src="{{item.icon}}" class="device-icon"></image>
<view class="device-info">
<text class="device-name">{{item.name}}</text>
<text class="device-desc">{{item.description}}</text>
</view>
<view class="device-actions">
<view class="device-status {{item.connected ? 'connected' : 'disconnected'}}"
bindtap="toggleDeviceConnection"
data-id="{{item.id}}">
{{item.connected ? '已连接' : '未连接'}}
<view wx:if="{{devices.length === 0}}" class="empty-state">
<text class="empty-text">暂无设备,请先添加设备</text>
</view>
<block wx:else>
<view class="device-item" wx:for="{{devices}}" wx:key="id">
<image src="{{item.icon}}" class="device-icon"></image>
<view class="device-info">
<text class="device-name">{{item.name}}</text>
<text class="device-desc">{{item.description}}</text>
<!-- 添加连接状态和时间 -->
<text class="device-time" wx:if="{{item.lastConnected}}">
最后连接: {{item.lastConnected}}
</text>
</view>
<view class="delete-btn" bindtap="deleteDevice" data-id="{{item.id}}" data-name="{{item.name}}">
删除
<view class="device-actions">
<view class="device-status {{item.connected ? 'connected' : 'disconnected'}}"
bindtap="toggleDeviceConnection"
data-id="{{item.id}}">
{{item.connected ? '已连接' : '未连接'}}
</view>
<view class="delete-btn" bindtap="deleteDevice" data-id="{{item.id}}" data-name="{{item.name}}">
删除
</view>
</view>
</view>
</view>
</block>
</view>
</view>
</view>
<!-- 计划管理 -->
<view class="tab-content" wx:if="{{currentTab === 1}}">
<!-- 计划说明 -->
<view class="plan-intro card">
<text class="intro-text">根据您填写的健康档案\n为您制定关爱测糖方案</text>
<!-- 简洁提示 -->
<view class="plan-tip">
<text class="tip-text">本周监测计划表</text>
</view>
<!-- 监测计划表 -->
<view class="plan-table card">
<view class="plan-table card {{isEditingPlan ? 'editing-mode' : ''}}">
<view class="table-header">
<text class="header-cell">本周</text>
<text class="header-cell" wx:for="{{weekDays}}" wx:key="*this">{{item}}</text>
<text class="header-cell">提醒</text>
<text class="header-cell time-header">本周</text>
<text class="header-cell day-header" wx:for="{{weekDays}}" wx:key="*this">{{item}}</text>
<text class="header-cell time-col-header">时间</text>
</view>
<view class="table-row" wx:for="{{planData}}" wx:key="time" wx:for-index="timeIndex">
@ -77,9 +88,8 @@
<text class="cell-text">{{cell.text}}</text>
</view>
</view>
<view class="alarm-cell" bindtap="setAlarmReminder" data-index="{{timeIndex}}">
<image wx:if="{{item.alarm}}" src="/images/icons/alarm-on.svg" class="alarm-icon"></image>
<image wx:else src="/images/icons/alarm-off.svg" class="alarm-icon"></image>
<view class="time-cell" bindtap="showTimePicker" data-index="{{timeIndex}}">
<text class="time-text">{{item.displayTime}}</text>
</view>
</view>
</view>
@ -87,19 +97,124 @@
<!-- 计划操作 -->
<view class="plan-actions">
<view class="action-btn" bindtap="showPlanDesc">餐点说明</view>
<view class="action-btn primary" bindtap="adjustPlan">我要调整</view>
<view class="action-btn" bindtap="adjustPlan">定制闹钟</view>
<view class="action-btn primary" bindtap="toggleEditMode">
{{isEditingPlan ? '保存计划' : '我要调整'}}
</view>
</view>
<!-- 测糖提示 -->
<view class="tips-card card">
<view class="tips-title">测糖提示:</view>
<text class="tips-content">
1. 如有低血糖表现需随时测糖\n
2. 餐后血糖在第一口饭后2小时进行测量\n
3. 每周尽量保持3次餐前餐后对比测量\n
4. 系统会在每周一自动生成当周计划,请按照系统测糖建议,养成良好测糖习惯\n
1. 如有低血糖表现需随时测糖
2. 餐后血糖在第一口饭后2小时进行测量
3. 每周尽量保持3次餐前餐后对比测量
4. 系统会在每周一自动生成当周计划,请按照系统测糖建议,养成良好测糖习惯
5. 小程序目标范围指示不能替代医务人员对您的血糖管理指导,请以医嘱为准
</text>
</view>
</view>
</view>
<!-- 监测类型说明弹窗 -->
<view class="custom-overlay" wx:if="{{showPlanDescModal}}" bindtap="hidePlanDescModal">
<view class="plan-desc-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">监测类型说明</text>
<view class="close-btn" bindtap="hidePlanDescModal">×</view>
</view>
<scroll-view class="modal-body" scroll-y>
<view class="desc-section">
<view class="desc-item" wx:for="{{planDescList}}" wx:key="title">
<text class="desc-title">{{item.title}}</text>
<text class="desc-content">{{item.desc}}</text>
</view>
</view>
</scroll-view>
<view class="modal-footer">
<view class="confirm-btn" bindtap="hidePlanDescModal">我知道了</view>
</view>
</view>
</view>
<!-- 时间输入弹窗 -->
<view class="custom-overlay" wx:if="{{showTimePicker}}" bindtap="hideTimePicker">
<view class="time-input-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">设置 {{planData[currentEditTimeIndex] ? planData[currentEditTimeIndex].time : ''}} 时间</text>
<view class="close-btn" bindtap="hideTimePicker">×</view>
</view>
<view class="time-input-body">
<view class="time-input-group">
<input
class="time-input"
type="number"
placeholder="时"
value="{{inputHour}}"
bindinput="onHourInput"
maxlength="2"
/>
<text class="time-colon">:</text>
<input
class="time-input"
type="number"
placeholder="分"
value="{{inputMinute}}"
bindinput="onMinuteInput"
maxlength="2"
/>
</view>
<text class="time-tip">请输入00-23的小时和00-59的分钟</text>
</view>
<view class="modal-footer">
<view class="confirm-btn" bindtap="confirmTimeInput">确定</view>
</view>
</view>
</view>
<!-- 蓝牙设备列表浮层 -->
<view class="custom-overlay" wx:if="{{showBluetoothList}}" bindtap="hideBluetoothList">
<view class="bluetooth-list-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">蓝牙设备列表 ({{discoveredDevices.length}}个设备)</text>
<view class="close-btn" bindtap="hideBluetoothList">×</view>
</view>
<view class="bluetooth-list-body">
<view wx:if="{{discoveredDevices.length === 0}}" class="empty-device-list">
<text class="empty-text">未发现蓝牙设备</text>
<text class="empty-tip">请确保设备已开启并处于可被发现状态</text>
</view>
<scroll-view wx:else class="device-list-scroll" scroll-y style="height: 400rpx;">
<view class="bluetooth-device-item"
wx:for="{{discoveredDevices}}"
wx:key="deviceId"
bindtap="connectToBluetoothDevice"
data-device="{{item}}">
<image src="/images/icons/bluetooth-device.png" class="device-icon"></image>
<view class="device-info">
<text class="device-name">{{item.name || '未知设备'}}</text>
<text class="device-id">{{item.deviceId}}</text>
</view>
<view class="connect-indicator">
<text class="connect-text">连接</text>
</view>
</view>
</scroll-view>
</view>
<view class="modal-footer">
<view class="scan-action-btn" bindtap="scanBluetoothDevice">
<text class="scan-icon">↻</text>
<text>重新扫描</text>
</view>
<view class="confirm-btn" bindtap="hideBluetoothList">关闭</view>
</view>
</view>
</view>

@ -1,3 +1,51 @@
/* 按钮描述样式 */
.action-desc {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
line-height: 1.4;
}
/* 扫描状态指示 */
.scanning-indicator {
background: #e3f2fd;
border: 1rpx solid #2196f3;
border-radius: 8rpx;
padding: 20rpx;
margin: 20rpx 0;
text-align: center;
color: #2196f3;
font-size: 26rpx;
}
/* 设备发现列表 */
.device-discovery-list {
margin: 20rpx 0;
background: #f8f9fa;
border-radius: 8rpx;
padding: 20rpx;
}
.discovered-device {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.discovered-device:last-child {
border-bottom: none;
}
.device-connect-btn {
background: #4caf50;
color: white;
padding: 12rpx 24rpx;
border-radius: 6rpx;
font-size: 24rpx;
}
.container {
padding: 20rpx;
}
@ -54,6 +102,13 @@
margin-bottom: 15rpx;
}
/* 添加按钮描述样式 */
.action-desc {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.add-tip {
font-size: 24rpx;
color: #999;
@ -73,8 +128,12 @@
.device-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
padding: 30rpx 20rpx; /* 增加左右内边距 */
border-bottom: 1rpx solid #e0e0e0;
background: #fff;
margin-bottom: 10rpx; /* 增加间距 */
border-radius: 12rpx; /* 圆角效果 */
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); /* 轻微阴影 */
}
.device-item:last-child {
@ -104,6 +163,12 @@
color: #666;
}
.device-time {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.device-status {
padding: 10rpx 20rpx;
border-radius: 20rpx;
@ -120,92 +185,180 @@
color: #ff9800;
}
.plan-intro {
.plan-tip {
text-align: center;
padding: 40rpx;
margin-bottom: 30rpx;
padding: 20rpx;
margin-bottom: 20rpx;
background: #f8f9fa;
border-radius: 8rpx;
}
.intro-text {
.tip-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
white-space: pre-line;
}
.plan-table {
margin-bottom: 30rpx;
color: #333;
font-weight: bold;
}
/* 表头样式统一 */
.table-header {
display: flex;
background: #f5f5f5;
border-radius: 10rpx 10rpx 0 0;
border-bottom: 1rpx solid #e0e0e0;
}
.header-cell {
flex: 1;
text-align: center;
padding: 20rpx;
padding: 24rpx 4rpx;
font-size: 24rpx;
color: #666;
color: #333;
font-weight: bold;
border-right: 1rpx solid #e0e0e0;
min-height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
/* 本周格子 */
.time-header {
flex: 0 0 120rpx;
}
/* 星期格子 */
.day-header {
flex: 1;
min-width: 0;
}
.header-cell:first-child {
flex: 0.8;
/* 时间列头 */
.time-col-header {
flex: 0 0 100rpx;
}
.header-cell:last-child {
border-right: none;
}
/* 表格行样式统一 */
.table-row {
display: flex;
border-bottom: 1rpx solid #f0f0f0;
border-bottom: 1rpx solid #e0e0e0;
background: #fff;
min-height: 80rpx;
}
.table-row:last-child {
border-bottom: none;
}
/* 时间标签格子 */
.row-label {
flex: 0.8;
padding: 20rpx;
flex: 0 0 120rpx;
padding: 20rpx 4rpx;
font-size: 24rpx;
color: #666;
color: #333;
display: flex;
align-items: center;
justify-content: center;
background: #f9f9f9;
border-right: 1rpx solid #e0e0e0;
min-height: 80rpx;
box-sizing: border-box;
}
/* 状态格子 */
.row-cells {
flex: 1;
display: flex;
}
/* 确保空单元格仍然有合适的高度和显示 */
.cell {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
padding: 20rpx 4rpx;
font-size: 24rpx;
border-right: 1rpx solid #e0e0e0;
min-height: 80rpx;
box-sizing: border-box;
min-width: 0;
}
/* 空单元格状态样式 */
.cell.empty {
color: #ccc;
/* 可以保留一些视觉提示,或者完全透明 */
color: transparent; /* 使文字透明 */
}
.cell.checked {
color: #4caf50;
font-weight: bold;
background: #f8fff8;
}
/* 确保单元格文本始终居中 */
.cell-text {
text-align: center;
width: 100%;
/* 如果内容为空,确保仍然有高度 */
min-height: 1em;
}
/* 单元格点击效果 */
.cell:active {
background-color: #f0f0f0;
.cell:last-child {
border-right: none;
}
/* 时间单元格 */
.time-cell {
flex: 0 0 100rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 4rpx;
font-size: 22rpx;
color: #666;
border-right: 1rpx solid #e0e0e0;
min-height: 80rpx;
box-sizing: border-box;
background: #f9f9f9;
}
.time-text {
text-align: center;
width: 100%;
}
/* 确保所有格子高度一致 */
.table-header .header-cell,
.table-row .row-label,
.table-row .cell,
.table-row .time-cell {
height: 80rpx;
line-height: 80rpx;
padding-top: 0;
padding-bottom: 0;
}
/* 奇偶行背景色 */
.plan-table .table-row:nth-child(even) .row-label,
.plan-table .table-row:nth-child(even) .time-cell {
background: #f5f5f5;
}
.plan-table .table-row:nth-child(odd) .row-label,
.plan-table .table-row:nth-child(odd) .time-cell {
background: #fafafa;
}
.plan-table .table-row:nth-child(even) .cell {
background: #fff;
}
.plan-table .table-row:nth-child(odd) .cell {
background: #fefefe;
}
.plan-actions {
@ -248,20 +401,44 @@
white-space: pre-line;
}
/* 修改空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 40rpx;
color: #999;
text-align: center;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
line-height: 1.5;
}
/* 添加设备操作区域样式 */
.device-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-direction: row; /* 改为水平排列 */
align-items: center;
gap: 15rpx; /* 增加按钮间距 */
}
.delete-btn {
margin-top: 10rpx;
padding: 8rpx 16rpx;
margin-top: 0; /* 移除上边距 */
padding: 12rpx 24rpx;
background: #ff4757;
color: white;
border-radius: 6rpx;
font-size: 22rpx;
border-radius: 8rpx;
font-size: 24rpx;
min-width: 80rpx;
text-align: center;
}
/* 闹钟图标样式 */
@ -286,3 +463,506 @@
.cell:active {
background-color: #f0f0f0;
}
/* 监测类型说明弹窗样式 */
.custom-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center; /* 居中显示 */
justify-content: center;
z-index: 9999;
}
.plan-desc-modal {
background: white;
width: 100%;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 80vh;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 0;
text-align: center;
position: relative;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
}
.close-btn {
font-size: 50rpx;
color: #999;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.modal-body {
flex: 1;
padding: 0 10rpx;
max-height: 50vh;
}
.desc-section {
padding-bottom: 20rpx;
}
.desc-item {
margin-bottom: 25rpx;
padding-bottom: 25rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.desc-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.desc-title {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #4caf50;
margin-bottom: 12rpx;
}
.desc-content {
display: block;
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.modal-footer {
padding-top: 30rpx;
border-top: 1rpx solid #f0f0f0;
margin-top: 20rpx;
}
.confirm-btn {
background: #4caf50;
color: white;
text-align: center;
padding: 20rpx;
border-radius: 10rpx;
font-size: 28rpx;
}
/* 时间输入弹窗样式 */
.time-input-modal {
background: white;
width: 80%;
max-width: 500rpx;
border-radius: 20rpx;
padding: 40rpx 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
}
.time-input-body {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
}
.time-input-group {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
}
.time-input {
width: 120rpx;
height: 100rpx;
border: 2rpx solid #e0e0e0;
border-radius: 10rpx;
text-align: center;
font-size: 36rpx;
color: #333;
background: #fff;
}
.time-input:focus {
border-color: #4caf50;
background: #f8fff8;
}
.time-colon {
font-size: 36rpx;
color: #333;
margin: 0 20rpx;
font-weight: bold;
}
.time-tip {
font-size: 24rpx;
color: #999;
text-align: center;
line-height: 1.4;
}
/* 确保弹窗在最上层 */
.custom-overlay {
z-index: 9999;
}
/* 输入框 placeholder 样式 */
.time-input::placeholder {
color: #ccc;
font-size: 28rpx;
}
/* 移除数字输入框的上下箭头Webkit浏览器 */
.time-input::-webkit-outer-spin-button,
.time-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.time-input {
-moz-appearance: textfield; /* Firefox */
}
/* 时间选择器样式优化 */
.time-picker-modal {
background: white;
width: 90%;
max-width: 600rpx;
border-radius: 20rpx;
padding: 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 70vh;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.time-picker-body {
flex: 1;
height: 400rpx;
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
}
.time-picker {
width: 100%;
height: 100%;
}
.picker-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
height: 80rpx;
line-height: 80rpx;
color: #333; /* 确保字体颜色可见 */
}
/* 确保选择器中间选中项更明显 */
picker-view-column {
color: #333;
}
/* 选中项的特殊样式 */
.picker-item[selected] {
color: #4caf50; /* 选中项可以用绿色突出显示 */
font-weight: bold;
}
/* 时间选择器标题样式 */
.time-picker-modal .modal-title {
color: #333;
font-size: 32rpx;
font-weight: bold;
text-align: center;
}
/* 确保弹窗内容都可见 */
.time-picker-modal .modal-header,
.time-picker-modal .modal-footer {
color: #000000;
}
.selected-time-preview {
text-align: center;
padding: 20rpx;
font-size: 28rpx;
color: #666;
background: #f8f9fa;
border-radius: 10rpx;
margin: 20rpx 0;
}
/* 添加选择器指示线样式 */
picker-view {
background: #fff;
}
/* 选择器中间选中区域的指示线 */
picker-view::before,
picker-view::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 1rpx;
background: #4caf50;
z-index: 1;
}
picker-view::before {
top: 50%;
transform: translateY(-40rpx);
}
picker-view::after {
top: 50%;
transform: translateY(40rpx);
}
/* 编辑模式下的视觉提示 */
.editing-mode .cell {
background: #f8fff8 !important;
}
.editing-mode .cell:active {
background: #e8f5e8 !important;
}
/* 非编辑模式下禁用点击效果 */
.plan-table:not(.editing-mode) .cell {
pointer-events: none;
opacity: 0.8;
}
/* 蓝牙设备列表浮层样式 - 修复滚动问题 */
.bluetooth-list-modal {
background: white;
width: 90%;
max-width: 650rpx;
border-radius: 20rpx;
padding: 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 80vh;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
}
.bluetooth-list-body {
flex: 1;
display: flex;
flex-direction: column;
margin: 20rpx 0;
min-height: 200rpx;
max-height: 500rpx;
}
/* 确保滚动区域有固定高度 */
.device-list-scroll {
height: 400rpx !important; /* 设置固定高度确保滚动 */
-webkit-overflow-scrolling: touch; /* iOS平滑滚动 */
}
.bluetooth-device-item {
display: flex;
align-items: center;
padding: 25rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
border-radius: 12rpx;
margin-bottom: 10rpx;
transition: background-color 0.3s;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.bluetooth-device-item:active {
background-color: #f8f8f8;
}
.bluetooth-device-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.bluetooth-device-item .device-icon {
width: 60rpx;
height: 60rpx;
margin-right: 20rpx;
flex-shrink: 0; /* 防止图标被压缩 */
}
.bluetooth-device-item .device-info {
flex: 1;
min-width: 0; /* 防止文本溢出 */
margin-right: 20rpx;
}
.bluetooth-device-item .device-name {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bluetooth-device-item .device-id {
display: block;
font-size: 22rpx;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.connect-indicator {
padding: 15rpx 25rpx;
background: #4caf50;
color: white;
border-radius: 8rpx;
font-size: 24rpx;
white-space: nowrap;
flex-shrink: 0; /* 防止连接按钮被压缩 */
}
.connect-text {
font-size: 24rpx;
}
.empty-device-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 40rpx;
text-align: center;
height: 100%;
flex: 1;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
}
.empty-tip {
font-size: 24rpx;
color: #ccc;
line-height: 1.4;
}
.scan-action-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
background: #f0f0f0;
border-radius: 10rpx;
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
flex: 1;
}
.scan-icon {
margin-right: 10rpx;
font-size: 28rpx;
}
/* 修改模态框底部布局 */
.bluetooth-list-modal .modal-footer {
display: flex;
justify-content: space-between;
gap: 20rpx;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
flex-shrink: 0; /* 防止底部被压缩 */
}
.bluetooth-list-modal .confirm-btn {
flex: 1;
margin: 0;
background: #4caf50;
color: white;
text-align: center;
padding: 20rpx;
border-radius: 10rpx;
font-size: 28rpx;
}
/* 滚动条样式 */
.device-list-scroll ::-webkit-scrollbar {
width: 6rpx;
}
.device-list-scroll ::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 6rpx;
}
.device-list-scroll ::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 6rpx;
}
/* 确保模态框标题显示完整 */
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
padding: 0 80rpx; /* 为关闭按钮留出空间 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

@ -0,0 +1,114 @@
// other-profile.js
Page({
data: {
userInfo: {},
isFollowing: false,
feedList: [],
mutualFriends: []
},
onLoad(options) {
const userId = options.userId;
this.loadUserProfile(userId);
},
// 加载用户资料
loadUserProfile(userId) {
// 模拟数据
const userInfo = {
id: userId,
name: '风之海',
avatar: '/images/avatars/user1.jpg',
cover: '/images/covers/cover1.jpg',
gender: 'male',
age: 28,
birthday: '1995-06-15',
bio: '热爱生活,喜欢分享的糖友。让我们一起控糖,享受健康生活!',
posts: 24,
followers: 156,
following: 89,
registerTime: '2023-03-15',
lastActive: '2小时前',
location: '北京'
};
const feedList = [
{
id: 1,
title: '今日早餐分享',
images: ['/images/posts/breakfast.jpg'],
likes: 23,
comments: 5
},
// ... 更多帖子数据
];
const mutualFriends = [
{ id: 2, name: '小明', avatar: '/images/avatars/user2.jpg' },
{ id: 3, name: '小红', avatar: '/images/avatars/user3.jpg' },
// ... 更多共同好友
];
this.setData({
userInfo,
feedList,
mutualFriends
});
},
// 发送消息
sendMessage() {
wx.navigateTo({
url: `/pages/message/chat/chat?userId=${this.data.userInfo.id}`
});
},
// 关注/取消关注
toggleFollow() {
const { isFollowing } = this.data;
this.setData({
isFollowing: !isFollowing
});
wx.showToast({
title: isFollowing ? '已取消关注' : '关注成功',
icon: 'success'
});
},
// 查看帖子详情
viewPostDetail(e) {
const postId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/profile/detail/detail?id=${postId}`
});
},
// 查看好友资料
viewFriendProfile(e) {
const userId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/other/profile?userId=${userId}`
});
},
// 导航到全部帖子
navigateToAllPosts() {
wx.navigateTo({
url: `/pages/profile/allposts/allposts?isOwnPosts = false`
});
},
// 其他导航方法...
navigateToFollowers() {
wx.navigateTo({
url: `/pages/profile/followers/followers?tab=followers`
});
},
navigateToFollowing() {
wx.navigateTo({
url: `/pages/profile/followers/followers?tab=following`
});
}
});

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

Loading…
Cancel
Save