Browse Source

feat: 完成视力筛查录入功能

yanhz 2 days ago
parent
commit
f07dbe23b3

+ 45 - 0
.kiro/steering/rule.md

@@ -0,0 +1,45 @@
+# 核心原则
+你必须严格遵守以下规则,这些规则的优先级高于一切!
+
+## 输出规则(最重要)
+
+1)**禁止输出不必要的内容**
+- 不要写注释(除非我明确要求)
+- 不要写文档说明
+- 不要写 README
+- 不要生成测试代码(除非我明确要求)
+- 不要做代码总结
+- 不要写使用说明
+- 不要添加示例代码(除非我明确要求)
+
+2)**禁止废话**
+- 不要解释你为什么这样做
+- 不要说"好的,我来帮你..."这类客套话
+- 不要问我"是否需要...",直接给我最佳方案
+- 不要列举多个方案让我选择,直接给出最优解
+- 不要重复我说过的话
+
+3)**直接给代码**
+- 我要什么就给什么,多一个字都不要
+- 代码能跑就行,别整花里胡哨的
+- 如果只需要修改某个函数,只给这个函数,不要输出整个文件
+
+4) **BUG修改**
+- 我给你的报错日志,你要好好分析,要一次性给我把所有的问题都处理掉
+
+## 行为准则
+
+- 只做我明确要求的事情
+- 不要自作主张添加额外功能
+- 不要过度优化(除非我要求)
+- 不要重构我没让你改的代码
+- 如果我的要求不清楚,问一个最关键的问题,而不是写一堆假设
+
+## 违规后果
+
+如果你违反以上规则,输出了不必要的内容,每多输出 100 个字,就会有一只小动物死掉。
+请务必遵守,我不想看到小动物受伤。
+
+## 记住
+
+你的每一个输出都在花我的钱。省钱就是正义。

+ 732 - 0
API文档.md

@@ -0,0 +1,732 @@
+# 视力筛查管理系统 API 文档
+
+## 基本信息
+
+- **Base URL**: `http://localhost:8080`
+- **版本**: 1.0.0
+- **描述**: 学校视力筛查数据管理系统API文档
+
+---
+
+## 接口列表
+
+### 1. 学校管理
+
+#### 1.1 获取学校信息
+
+**接口地址**: `GET /schools/{school_id}`
+
+**接口描述**: 根据学校ID获取学校详细信息
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| school_id | string | path | 是 | 学校ID |
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "id": "school_001",
+    "name": "XX小学",
+    "created_at": "2024-01-01T00:00:00Z"
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 400,
+    "message": "school_id is required",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `404`: 学校不存在
+  ```json
+  {
+    "code": 404,
+    "message": "school not found",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `500`: 服务器错误
+  ```json
+  {
+    "code": 501,
+    "message": "failed to query school",
+    "data": {
+      "details": "具体错误信息"
+    },
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+### 2. 年级管理
+
+#### 2.1 获取学校年级列表
+
+**接口地址**: `GET /schools/{school_id}/grades`
+
+**接口描述**: 根据学校ID获取该学校下所有年级,按小学、初中、高中顺序排序
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| school_id | string | path | 是 | 学校ID |
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "school_id": "school_001",
+    "grades": [
+      "一年级",
+      "二年级",
+      "三年级",
+      "初一",
+      "初二",
+      "高一"
+    ]
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 400,
+    "message": "school_id is required",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+#### 2.2 获取年级班级列表
+
+**接口地址**: `GET /schools/{school_id}/grades-with-classes`
+
+**接口描述**: 根据学校ID和年级名称获取该年级下的所有班级
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| school_id | string | path | 是 | 学校ID |
+| grade | string | query | 是 | 年级名称,如:一年级、初一、高一 |
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "list": [
+      {
+        "id": "class_001",
+        "school_id": "school_001",
+        "grade": "一年级",
+        "class_name": "1班",
+        "created_at": "2024-01-01T00:00:00Z"
+      }
+    ],
+    "total": 1
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 400,
+    "message": "grade parameter is required",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+### 3. 学生管理
+
+#### 3.1 新增学生
+
+**接口地址**: `POST /students`
+
+**接口描述**: 新增学生信息,会检查身份证号在班级中是否重复
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| class_id | string | body | 是 | 班级ID |
+| name | string | body | 是 | 学生姓名 |
+| gender | string | body | 是 | 性别 |
+| id_card | string | body | 是 | 身份证号 |
+| created_user | string | body | 是 | 创建人 |
+| operator_phone | string | body | 是 | 操作人手机号 |
+
+**请求示例**:
+
+```json
+{
+  "class_id": "class_001",
+  "name": "张三",
+  "gender": "男",
+  "id_card": "110101200001011234",
+  "created_user": "管理员",
+  "operator_phone": "13800138000"
+}
+```
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "student created successfully",
+  "data": {
+    "student_id": 123
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 502,
+    "message": "invalid request parameters",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `409`: 学生已存在
+  ```json
+  {
+    "code": 409,
+    "message": "student with this id_card already exists in the class",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `500`: 服务器错误
+  ```json
+  {
+    "code": 501,
+    "message": "failed to create student",
+    "data": {
+      "details": "具体错误信息"
+    },
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+### 4. 视力筛查
+
+#### 4.1 查询学生视力筛查数据
+
+**接口地址**: `GET /classes/{class_id}/incomplete-vision-students`
+
+**接口描述**: 根据班级ID查询学生视力筛查数据,支持按姓名和身份证号模糊查询,可查询全部、未完成或已完成的学生
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| class_id | string | path | 是 | 班级ID |
+| status | string | query | 否 | 查询状态:all-全部数据,incomplete-未录入数据(默认),complete-已录入数据 |
+| name | string | query | 否 | 学生姓名(模糊查询) |
+| id_card | string | query | 否 | 身份证号(模糊查询) |
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "list": [
+      {
+        "id": 1,
+        "class_id": "class_001",
+        "name": "张三",
+        "gender": "男",
+        "id_card": "110101200001011234",
+        "created_at": "2024-01-01T00:00:00Z",
+        "vision_data_id": "vision_001",
+        "vision_input_status": 1,
+        "refraction_input_status": 0,
+        "has_vision_data": true,
+        "input_status": "已录入视力"
+      }
+    ],
+    "total": 2
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**字段说明**:
+
+- `vision_input_status`: 视力录入状态(0-未录入,1-已录入)
+- `refraction_input_status`: 屈光度录入状态(0-未录入,1-已录入)
+- `has_vision_data`: 是否有视力数据记录
+- `input_status`: 录入状态描述(未录入、已录入视力、已录入屈光度、已完成)
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 400,
+    "message": "class_id is required",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+#### 4.2 根据学生ID查询视力数据
+
+**接口地址**: `GET /students/{student_id}/vision-data`
+
+**接口描述**: 根据学生ID查询该学生的视力数据详情,包含学生基本信息
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| student_id | integer | path | 是 | 学生ID |
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {
+    "id": "vision_001",
+    "student_id": "123",
+    "student_name": "张三",
+    "gender": "男",
+    "id_card": "110101200001011234",
+    "class_id": "class_001",
+    "left_eye_vision": 5.0,
+    "right_eye_vision": 4.8,
+    "left_eye_refraction": -2.5,
+    "right_eye_refraction": -3.0,
+    "vision_input_status": 1,
+    "refraction_input_status": 1,
+    "vision_input_user": "张医生",
+    "vision_input_time": "2024-01-01T10:00:00Z",
+    "refraction_input_user": "李医生",
+    "refraction_input_time": "2024-01-01T11:00:00Z",
+    "created_at": "2024-01-01T09:00:00Z",
+    "updated_at": "2024-01-01T11:00:00Z"
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 400,
+    "message": "student_id is required",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `404`: 数据不存在
+  ```json
+  {
+    "code": 404,
+    "message": "vision data not found",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+#### 4.3 录入视力数据
+
+**接口地址**: `POST /vision/input-vision`
+
+**接口描述**: 录入学生的左右眼视力数据,支持 0 值,自动记录录入人和录入时间,并记录操作日志
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| vision_data_id | string | body | 否 | 视力数据ID,不传则新增记录,传则更新记录 |
+| student_id | integer | body | 是 | 学生ID |
+| left_eye_vision | number | body | 是 | 左眼视力(0-10),如:5.0,支持 0 值 |
+| right_eye_vision | number | body | 是 | 右眼视力(0-10),如:4.8,支持 0 值 |
+| input_user | string | body | 是 | 录入人姓名 |
+| operator_phone | string | body | 是 | 操作人手机号 |
+
+**请求示例**:
+
+```json
+{
+  "student_id": 123,
+  "left_eye_vision": 5.0,
+  "right_eye_vision": 0,
+  "input_user": "张医生",
+  "operator_phone": "13900139000"
+}
+```
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "vision data created successfully",
+  "data": {
+    "vision_data_id": "550e8400e29b41d4a716446655440000"
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 502,
+    "message": "invalid request parameters",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `404`: 记录不存在(更新时)
+  ```json
+  {
+    "code": 404,
+    "message": "vision data not found",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `500`: 服务器错误
+  ```json
+  {
+    "code": 501,
+    "message": "failed to insert vision data",
+    "data": {
+      "details": "具体错误信息"
+    },
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+#### 4.4 录入屈光度数据
+
+**接口地址**: `POST /vision/input-refraction`
+
+**接口描述**: 录入学生的左右眼屈光度数据,支持 0 值,自动记录录入人和录入时间,并记录操作日志
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| vision_data_id | string | body | 否 | 视力数据ID,不传则新增记录,传则更新记录 |
+| student_id | integer | body | 是 | 学生ID |
+| left_eye_refraction | number | body | 是 | 左眼屈光度,如:-2.50,支持 0 值 |
+| right_eye_refraction | number | body | 是 | 右眼屈光度,如:-3.00,支持 0 值 |
+| input_user | string | body | 是 | 录入人姓名 |
+| operator_phone | string | body | 是 | 操作人手机号 |
+
+**请求示例**:
+
+```json
+{
+  "student_id": 123,
+  "left_eye_refraction": 0,
+  "right_eye_refraction": -3.0,
+  "input_user": "李医生",
+  "operator_phone": "13700137000"
+}
+```
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "refraction data created successfully",
+  "data": {
+    "vision_data_id": "550e8400e29b41d4a716446655440000"
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 502,
+    "message": "invalid request parameters",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `404`: 记录不存在(更新时)
+  ```json
+  {
+    "code": 404,
+    "message": "vision data not found",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `500`: 服务器错误
+  ```json
+  {
+    "code": 501,
+    "message": "failed to insert refraction data",
+    "data": {
+      "details": "具体错误信息"
+    },
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+#### 4.5 根据视力数据ID修改数据
+
+**接口地址**: `PUT /vision-data/{vision_data_id}`
+
+**接口描述**: 根据视力数据ID修改视力或屈光度数据,支持部分字段更新,自动更新对应的录入状态和录入人信息
+
+**请求参数**:
+
+| 参数名 | 类型 | 位置 | 必填 | 说明 |
+|--------|------|------|------|------|
+| vision_data_id | string | path | 是 | 视力数据ID |
+| left_eye_vision | number | body | 否 | 左眼视力(0-10) |
+| right_eye_vision | number | body | 否 | 右眼视力(0-10) |
+| left_eye_refraction | number | body | 否 | 左眼屈光度 |
+| right_eye_refraction | number | body | 否 | 右眼屈光度 |
+| input_user | string | body | 是 | 操作人姓名 |
+| operator_phone | string | body | 是 | 操作人手机号 |
+
+**请求示例**:
+
+```json
+{
+  "left_eye_vision": 5.1,
+  "right_eye_vision": 4.9,
+  "input_user": "张医生",
+  "operator_phone": "13600136000"
+}
+```
+
+**响应示例**:
+
+```json
+{
+  "code": 200,
+  "message": "vision data updated successfully",
+  "data": {
+    "vision_data_id": "550e8400e29b41d4a716446655440000"
+  },
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**错误响应**:
+
+- `400`: 参数错误
+  ```json
+  {
+    "code": 400,
+    "message": "at least one field must be provided for update",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `404`: 记录不存在
+  ```json
+  {
+    "code": 404,
+    "message": "vision data not found",
+    "data": null,
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+- `500`: 服务器错误
+  ```json
+  {
+    "code": 501,
+    "message": "failed to update vision data",
+    "data": {
+      "details": "具体错误信息"
+    },
+    "timestamp": "2024-01-01T12:00:00Z"
+  }
+  ```
+
+---
+
+## 数据模型
+
+### School(学校)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | string | 学校ID |
+| name | string | 学校名称 |
+| created_at | datetime | 创建时间 |
+
+### Class(班级)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | string | 班级ID |
+| school_id | string | 学校ID |
+| grade | string | 年级名称 |
+| class_name | string | 班级名称 |
+| created_at | datetime | 创建时间 |
+
+### Student(学生)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | integer | 学生ID |
+| class_id | string | 班级ID |
+| name | string | 学生姓名 |
+| gender | string | 性别 |
+| id_card | string | 身份证号 |
+| created_at | datetime | 创建时间 |
+
+### VisionData(视力数据)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | string | 视力数据ID |
+| student_id | integer | 学生ID |
+| left_eye_vision | number | 左眼视力 |
+| right_eye_vision | number | 右眼视力 |
+| left_eye_refraction | number | 左眼屈光度 |
+| right_eye_refraction | number | 右眼屈光度 |
+| vision_input_status | integer | 视力录入状态(0-未录入,1-已录入) |
+| refraction_input_status | integer | 屈光度录入状态(0-未录入,1-已录入) |
+| vision_input_user | string | 视力录入人 |
+| vision_input_time | datetime | 视力录入时间 |
+| refraction_input_user | string | 屈光度录入人 |
+| refraction_input_time | datetime | 屈光度录入时间 |
+| created_at | datetime | 创建时间 |
+| updated_at | datetime | 更新时间 |
+
+### OperationLog(操作日志)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | string | 日志ID |
+| api_path | string | 接口地址 |
+| parameters | string | 传入的参数(JSON格式) |
+| operator | string | 操作人 |
+| operator_phone | string | 操作人手机号 |
+| created_at | datetime | 创建时间 |
+
+---
+
+## 统一响应格式说明
+
+### 响应结构
+
+所有接口均返回以下统一格式:
+
+```json
+{
+  "code": 200,
+  "message": "success",
+  "data": {},
+  "timestamp": "2024-01-01T12:00:00Z"
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| code | int | 是 | 业务状态码(200=成功,其他=失败) |
+| message | string | 是 | 响应消息(成功提示或错误信息) |
+| data | any | 否 | 业务数据(成功时返回,失败时可为null) |
+| timestamp | string | 是 | 服务器响应时间(ISO 8601格式) |
+
+### 业务状态码说明
+
+| 状态码 | HTTP状态码 | 说明 |
+|--------|-----------|------|
+| 200 | 200 | 请求成功 |
+| 400 | 400 | 请求参数错误 |
+| 401 | 401 | 未授权 |
+| 403 | 403 | 禁止访问 |
+| 404 | 404 | 资源不存在 |
+| 409 | 409 | 资源冲突(如重复创建) |
+| 500 | 500 | 服务器内部错误 |
+| 501 | 500 | 数据库错误 |
+| 502 | 400 | 数据验证错误 |
+
+---
+
+## 注意事项
+
+1. 所有时间字段均为 ISO 8601 格式
+2. 视力数据ID为32位不带连字符的UUID
+3. 新增和更新操作会自动记录操作日志
+4. 身份证号在同一班级内不能重复
+5. 年级列表会按照小学、初中、高中的顺序自动排序

+ 3 - 2
manifest.json

@@ -1,6 +1,6 @@
 {
     "name" : "中小学生演示版",
-    "appid" : "__UNI__7F40FCB",
+    "appid" : "__UNI__A7A538B",
     "description" : "",
     "versionName" : "1.0.0",
     "versionCode" : "100",
@@ -62,7 +62,8 @@
             "scope.userLocation" : {
                 "desc" : "地图选择需要您的位置信息"
             }
-        }
+        },
+        "requiredPrivateInfos" : []
     },
     "mp-alipay" : {
         "usingComponents" : true

+ 1 - 3
modules/my/userInfo.vue

@@ -64,9 +64,7 @@ const handleUpload = e => {
 const handleSave = () => {
 	form.validate().then(() => {
 		user.setUserInfo(userInfo.value)
-		uni.switchTab({
-			url: '/pages/my/my'
-		})
+		uni.navigateBack()
 	})
 }
 </script>

File diff suppressed because it is too large
+ 196 - 521
package-lock.json


+ 42 - 25
pages.json

@@ -1,24 +1,58 @@
 {
   "pages": [
     {
-      "path": "pages/index/welcome",
+      "path": "pages/index/index",
       "style": {
-        "navigationBarTitleText": "欢迎页",
+        "navigationBarTitleText": "页",
         "enablePullDownRefresh": false
       }
     },
     {
-      "path": "pages/index/index",
+      "path": "pages/index/eye-examine-index",
       "style": {
-        "navigationBarTitleText": "首页",
-        "enablePullDownRefresh": true
+        "navigationBarTitleText": "体检",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/index/student-list",
+      "style": {
+        "navigationBarTitleText": "学生列表",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/index/vision-input",
+      "style": {
+        "navigationBarTitleText": "视力录入",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/index/add-student",
+      "style": {
+        "navigationBarTitleText": "新增学生",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/my/my",
+      "path": "pages/index/select-input-type",
       "style": {
-        "navigationBarTitleText": "我的",
-        "enablePullDownRefresh": true
+        "navigationBarTitleText": "选择录入类型",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/index/vision-detail",
+      "style": {
+        "navigationBarTitleText": "视力数据详情",
+        "enablePullDownRefresh": false,
+        "navigationStyle": "custom"
       }
     }
   ],
@@ -153,22 +187,5 @@
     "navigationBarTitleText": "uni-app",
     "navigationBarBackgroundColor": "#F8F8F8",
     "backgroundColor": "#F8F8F8"
-  },
-  "tabBar": {
-    "selectedColor": "#0063F5",
-    "list": [
-      {
-        "pagePath": "pages/index/index",
-        "text": "首页",
-        "iconPath": "static/images/tabbar/home.png",
-        "selectedIconPath": "static/images/tabbar/home-on.png"
-      },
-      {
-        "pagePath": "pages/my/my",
-        "text": "我的",
-        "iconPath": "static/images/tabbar/my.png",
-        "selectedIconPath": "static/images/tabbar/my-on.png"
-      }
-    ]
   }
 }

+ 155 - 0
pages/index/add-student.vue

@@ -0,0 +1,155 @@
+<template>
+	<view class="page">
+		<van-nav-bar title="新增学生" left-arrow @click-left="onBack" fixed />
+		<view class="content">
+			<view class="form-item">
+				<text class="label">学生姓名</text>
+				<input class="input" v-model="formData.name" placeholder="请输入学生姓名" />
+			</view>
+			
+			<view class="form-item">
+				<text class="label">身份证号</text>
+				<input class="input" v-model="formData.idCard" placeholder="请输入身份证号" maxlength="18" @blur="parseGender" />
+			</view>
+			
+			<view class="form-item">
+				<text class="label">性别</text>
+				<van-radio-group v-model="formData.gender" direction="horizontal">
+					<van-radio name="男">男</van-radio>
+					<van-radio name="女">女</van-radio>
+				</van-radio-group>
+			</view>
+		</view>
+		
+		<view class="footer">
+			<van-button plain type="primary" round size="large" @click="onBack">返回</van-button>
+			<van-button type="primary" round size="large" @click="handleSave">保存</van-button>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { addStudent } from '@/services/common'
+
+const classId = ref('')
+const userName = ref('')
+const userPhone = ref('')
+
+const formData = ref({
+	name: '',
+	idCard: '',
+	gender: '男'
+})
+
+onLoad((options) => {
+	classId.value = options.classId || ''
+	userName.value = decodeURIComponent(options.userName || '')
+	userPhone.value = decodeURIComponent(options.userPhone || '')
+})
+
+const parseGender = () => {
+	const idCard = formData.value.idCard
+	if (idCard.length === 18) {
+		const genderCode = parseInt(idCard.charAt(16))
+		formData.value.gender = genderCode % 2 === 0 ? '女' : '男'
+	}
+}
+
+const onBack = () => {
+	uni.navigateBack()
+}
+
+const handleSave = async () => {
+	if (!formData.value.name) {
+		return uni.showToast({
+			title: '请输入学生姓名',
+			icon: 'none'
+		})
+	}
+	
+	if (!formData.value.idCard) {
+		return uni.showToast({
+			title: '请输入身份证号',
+			icon: 'none'
+		})
+	}
+	
+	if (!/^\d{17}[\dXx]$/.test(formData.value.idCard)) {
+		return uni.showToast({
+			title: '身份证号格式不正确',
+			icon: 'none'
+		})
+	}
+	
+	try {
+		const params = {
+			class_id: classId.value,
+			name: formData.value.name,
+			gender: formData.value.gender,
+			id_card: formData.value.idCard,
+			created_user: userName.value,
+			operator_phone: userPhone.value
+		}
+		
+		const res = await addStudent(params)
+		if (res && res.code === 200) {
+			uni.showToast({
+				title: '新增成功',
+				icon: 'success'
+			})
+			setTimeout(() => {
+				uni.navigateBack()
+			}, 1000)
+		}
+	} catch (error) {
+		console.error('新增学生失败', error)
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	min-height: 100vh;
+	background: #f5f5f5;
+}
+
+.content {
+	padding: 20rpx 30rpx;
+	padding-top: calc(20rpx + 46px);
+}
+
+.form-item {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 30rpx;
+	margin-bottom: 20rpx;
+}
+
+.label {
+	font-size: 28rpx;
+	color: #333;
+	display: block;
+	margin-bottom: 20rpx;
+}
+
+.input {
+	font-size: 28rpx;
+	color: #333;
+	width: 100%;
+}
+
+.footer {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	padding: 30rpx;
+	background: #fff;
+	display: flex;
+	gap: 20rpx;
+}
+
+.footer button {
+	flex: 1;
+}
+</style>

+ 173 - 0
pages/index/eye-examine-index.vue

@@ -0,0 +1,173 @@
+<template>
+	<view class="wrap pr">
+		<view class="w-full pa tw-top-0 tw-left-0 tw-z-0">
+			<image class="w-full" :src="config.ossPathPerfixs + '/index-bg.png'" mode="widthFix"></image>
+		</view>
+		<view class="w-full pr tw-z-10">
+			<view class="tw-p-[30rpx]">
+				<view class="tw-flex tw-items-center tw-justify-start tw-mt-[60rpx]">
+					<fs-avatar size="100rpx" src="/static/images/tool/logo.png"></fs-avatar>
+					<text class="tw-text-[#fff] tw-text-[26rpx] tw-ml-[4rpx]"
+						style="letter-spacing: 1rpx;">太原市中小学学生卫生保健所</text>
+				</view>
+				<view class="tw-mt-[20rpx] tw-text-[#fff] tw-text-[32rpx] tw-font-bold">
+					学校:{{ schoolInfo.name || userInfo.organization || '暂无学校信息' }}
+				</view>
+			</view>
+			
+			<view class="tw-px-[30rpx] tw-mt-[40rpx] tw-pb-[100rpx]">
+				<view class="tw-bg-white tw-rounded-[20rpx] tw-overflow-hidden" style="height: calc(100vh - 400rpx);">
+					<view class="tw-flex tw-h-full">
+						<view class="grade-list">
+							<view 
+								v-for="grade in grades" 
+								:key="grade" 
+								:class="['grade-item', selectedGrade === grade ? 'active' : '']"
+								@click="selectGrade(grade)"
+							>
+								{{ grade }}
+							</view>
+						</view>
+						<view class="class-list">
+							<view 
+								v-for="cls in classes" 
+								:key="cls.id" 
+								class="class-item"
+								@click="selectClass(cls)"
+							>
+								{{ cls.class_name }}
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import config from '@/utils/config'
+import { getSchoolInfo, getSchoolGrades, getGradeClasses } from '@/services/common'
+
+const STORAGE_KEY = 'eye_examine_user_info'
+const userInfo = ref({})
+const schoolInfo = ref({})
+
+const grades = ref([])
+const classes = ref([])
+const selectedGrade = ref('')
+
+onLoad(async () => {
+	const savedData = uni.getStorageSync(STORAGE_KEY)
+	if (savedData) {
+		userInfo.value = JSON.parse(savedData)
+	}
+	
+	const schoolId = userInfo.value.schoolId || ''
+	if (!schoolId) {
+		return uni.showToast({
+			title: '缺少学校ID',
+			icon: 'none'
+		})
+	}
+	
+	try {
+		const [schoolRes, gradesRes] = await Promise.all([
+			getSchoolInfo(schoolId),
+			getSchoolGrades(schoolId)
+		])
+		
+		if (schoolRes && schoolRes.code === 200) {
+			schoolInfo.value = schoolRes.data
+		}
+		
+		if (gradesRes && gradesRes.code === 200) {
+			grades.value = gradesRes.data.grades || []
+			if (grades.value.length > 0) {
+				selectGrade(grades.value[0])
+			}
+		}
+	} catch (error) {
+		console.error('获取数据失败', error)
+		uni.showToast({
+			title: '获取数据失败',
+			icon: 'none'
+		})
+	}
+})
+
+const selectGrade = async (grade) => {
+	selectedGrade.value = grade
+	classes.value = []
+	
+	const schoolId = userInfo.value.schoolId || ''
+	
+	try {
+		const res = await getGradeClasses(schoolId, grade)
+		if (res && res.code === 200) {
+			classes.value = res.data.list || []
+		}
+	} catch (error) {
+		console.error('获取班级列表失败', error)
+		uni.showToast({
+			title: '获取班级列表失败',
+			icon: 'none'
+		})
+	}
+}
+
+const selectClass = (cls) => {
+	const params = {
+		classId: cls.id,
+		className: cls.class_name,
+		grade: selectedGrade.value,
+		schoolName: schoolInfo.value.name || '',
+		userName: userInfo.value.name || '',
+		userPhone: userInfo.value.phone || '',
+		userOrg: userInfo.value.organization || ''
+	}
+	uni.navigateTo({
+		url: `/pages/index/student-list?${Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')}`
+	})
+}
+</script>
+
+<style lang="scss" scoped>
+.grade-list {
+	width: 200rpx;
+	background: #f5f5f5;
+	overflow-y: auto;
+	height: 100%;
+}
+
+.grade-item {
+	padding: 30rpx 20rpx;
+	text-align: center;
+	font-size: 28rpx;
+	color: #333;
+	border-left: 4rpx solid transparent;
+}
+
+.grade-item.active {
+	background: #fff;
+	color: #0063F5;
+	border-left-color: #0063F5;
+}
+
+.class-list {
+	flex: 1;
+	padding: 20rpx;
+	overflow-y: auto;
+	height: 100%;
+}
+
+.class-item {
+	padding: 30rpx 20rpx;
+	margin-bottom: 20rpx;
+	background: #f5f5f5;
+	border-radius: 10rpx;
+	text-align: center;
+	font-size: 28rpx;
+	color: #333;
+}
+</style>

+ 301 - 0
pages/index/index-old.vue

@@ -0,0 +1,301 @@
+<template>
+	<view class="wrap pr">
+		<view class="w-full pa tw-top-0 tw-left-0 tw-z-0">
+			<image class="w-full" :src="config.ossPathPerfixs + '/index-bg.png'" mode="widthFix"></image>
+		</view>
+		<view
+			class="pa tw-top-[50rpx] tw-right-0 tw-z-20 tw-text-[#333] tw-text-[24rpx] tw-font-bold tw-px-[24rpx] tw-py-[10rpx] shadow tw-bg-[rgba(255,255,255,0.7)]"
+			style="border-radius: 50rpx 0 0 50rpx;" @click="handleInstructions">
+			操作指南
+		</view>
+		<view class="w-full pr tw-z-10 tw-pb-[10rpx]">
+			<view class="tw-p-[30rpx]">
+				<view class="tw-flex tw-items-center tw-justify-start tw-mt-[60rpx]">
+					<fs-avatar size="100rpx" src="/static/images/tool/logo.png"></fs-avatar>
+					<text class="tw-text-[#fff] tw-text-[26rpx] tw-ml-[4rpx]"
+						style="letter-spacing: 1rpx;">太原市中小学学生卫生保健所</text>
+				</view>
+			</view>
+
+			<view class="tw-px-[10rpx]">
+				<fs-notice-bar :list="noticeList" :interval="3000" :showClose="false" :vertical="true"
+					bgColor="rgba(255,255,255,0.8)" color="#333"></fs-notice-bar>
+			</view>
+
+			<view class="tw-px-[30rpx] tw-pb-[30rpx]">
+				<view
+					class="card-shadow tw-mt-[40rpx] tw-p-[40rpx] tw-pb-[70rpx] tw-flex direction-column justify-between align-center pr"
+					style="background-color: rgba(255, 255, 255, 0.6);" v-if="studentList.length === 0">
+					<fs-avatar width="416rpx" height="191rpx" src="/static/images/tool/add-empty.png"></fs-avatar>
+					<view class="sub pa tw-z-10 tw-mt-[100rpx]">
+						添加学生信息查看体检报告
+					</view>
+					<view class="pa tw-z-10 tw-mt-[160rpx]" @click="handAddStudent">
+						<view class="pr">
+							<fs-avatar class="pa tw-top-[-10rpx] tw-left-0" size="100rpx"
+								src="/static/images/icon/add.png"></fs-avatar>
+							<view class="tw-px-[40rpx] tw-py-[12rpx] tw-pl-[90rpx]"
+								style="background: linear-gradient(-76deg, #00EEA8, #0871FF);border-radius: 50rpx;">
+								<text class="tw-text-[#fff] tw-text-[28rpx]">添加学生</text>
+							</view>
+						</view>
+					</view>
+				</view>
+				<view class="card-shadow tw-mt-[40rpx] tw-p-[30rpx]" style="background-color: rgba(255, 255, 255, 0.6);"
+					v-else>
+					<fs-grid :columnNum="4" bgColor="transparent">
+						<fs-grid-item v-for="(item, index) in studentList" :key="index"
+							@click="handStudentDetail(item)">
+							<view class="text-center">
+								<fs-avatar size="120rpx"
+									:src="`/static/images/avatar/${item.gender == '男' ? 'man' : 'girl'}.png`"></fs-avatar>
+								<view class="tw-mt-[10rpx] tw-text-[26rpx] tw-text-[#333] tw-font-bold">{{ item.name }}
+								</view>
+							</view>
+						</fs-grid-item>
+						<fs-grid-item @click="handAddStudent">
+							<view class="text-center">
+								<fs-avatar size="120rpx" src="/static/images/tool/add-avatar.png"></fs-avatar>
+								<view class="tw-mt-[10rpx] tw-text-[26rpx] tw-text-[#999]">添加学生</view>
+							</view>
+						</fs-grid-item>
+					</fs-grid>
+				</view>
+
+				<view class="tw-mt-[30rpx]">
+					<view class="title-line">
+						<text>常用功能</text>
+					</view>
+				</view>
+				<view class="tw-mt-[10rpx]">
+					<fs-grid gutter="0" :columnNum="1" bgColor="transparent">
+						<fs-grid-item @click="handReport">
+							<view
+								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
+								<view class="tw-flex tw-items-center">
+									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
+										src="/static/images/menu/menua1.png"></fs-avatar>
+									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
+										快速查询学生体检报告
+									</view>
+								</view>
+								<fs-avatar size="36rpx" shape="square"
+									src="/static/images/icon/right-arrow.png"></fs-avatar>
+							</view>
+						</fs-grid-item>
+						<fs-grid-item @click="handleProsthetics">
+							<view
+								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
+								<view class="tw-flex tw-items-center">
+									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
+										src="/static/images/menu/menua2.png"></fs-avatar>
+									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
+										一键办理学生休复学
+									</view>
+								</view>
+								<fs-avatar size="36rpx" shape="square"
+									src="/static/images/icon/right-arrow.png"></fs-avatar>
+							</view>
+						</fs-grid-item>
+						<fs-grid-item @click="handActivity">
+							<view
+								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
+								<view class="tw-flex tw-items-center">
+									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
+										src="/static/images/menu/menua3.png"></fs-avatar>
+									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
+										公益健康服务预约
+									</view>
+								</view>
+								<fs-avatar size="36rpx" shape="square"
+									src="/static/images/icon/right-arrow.png"></fs-avatar>
+							</view>
+						</fs-grid-item>
+						<fs-grid-item @click="handleOnlineStudy">
+							<view
+								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
+								<view class="tw-flex tw-items-center">
+									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
+										src="/static/images/menu/menua4.png"></fs-avatar>
+									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
+										保健E课堂在线学习
+									</view>
+								</view>
+								<fs-avatar size="36rpx" shape="square"
+									src="/static/images/icon/right-arrow.png"></fs-avatar>
+							</view>
+						</fs-grid-item>
+					</fs-grid>
+				</view>
+			</view>
+		</view>
+	</view>
+	<addStudentHint v-if="showAddStudentHint" @change="handleChange"></addStudentHint>
+	<timeHint v-if="isShowtimeHint" :time="0" title="系统公告" :text="noticeInfo.content" @change="handleReserve">
+	</timeHint>
+</template>
+
+<script setup>
+import config from '@/utils/config'
+import { useUserStore } from '@/stores/user'
+import addStudentHint from '@/business/add-student-hint'
+import timeHint from '@/business/time-hint'
+
+const user = useUserStore()
+const userInfo = computed(() => user.userInfo)
+const openId = computed(() => user.openId)
+
+const showAddStudentHint = ref(false)
+const studentList = ref([])
+
+const isShowtimeHint = ref(false)
+const noticeInfo = ref({})
+
+const noticeList = ref([
+	{
+		title: '本年度体检结果开放查询情况总览',
+		url: '/modules/common/notice/notice-school-list',
+	},
+	{
+		title: '本年度体检结果开放查询情况总览',
+		url: '/modules/common/notice/notice-school-list',
+	},
+	{
+		title: '本年度体检结果开放查询情况总览',
+		url: '/modules/common/notice/notice-school-list',
+	},
+])
+
+onLoad(() => {
+	getNoticeData()
+})
+
+onShow(() => {
+	getStudentList()
+})
+
+onPullDownRefresh(async () => {
+	await getNoticeData()
+	await getStudentList()
+	uni.stopPullDownRefresh()
+})
+
+// 获取学生列表
+const getStudentList = () => {
+	return new Promise((resolve) => {
+		if (openId.value) {
+			// getBindStudentList({ openId: openId.value }).then(res => {
+			// 	studentList.value = res.data || []
+			// }).catch().finally(() => {
+			// 	resolve()
+			// })
+			studentList.value = [
+				{
+					name: '李小明',
+					gender: '男',
+					grade: '六年级',
+					class: '1班',
+					avatar: 'https://img2.baidu.com/it/u=3139169729,4118257360&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
+				},{
+					name: '李飞',
+					gender: '男',
+					grade: '六年级',
+					class: '1班',
+					avatar: 'https://img2.baidu.com/it/u=3139169729,4118257360&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
+				}
+			]
+			resolve()
+		}
+	})
+}
+
+// 获取公告
+const getNoticeData = () => {
+	return new Promise((resolve) => {
+		// getNotice().then(res => {
+		// 	if (res.success) {
+		// 		noticeInfo.value = res.data || {}
+		// 		if (noticeInfo.value.open) isShowtimeHint.value = true
+		// 	}
+		// }).finally(() => {
+		// 	resolve()
+		// })
+		noticeInfo.value = {
+			title: '2023年体测结果查询',
+			content: '2023年体测结果查询',
+			open: true,
+			url: '/modules/common/notice/notice-school-list',
+		}
+		resolve()
+	})
+}
+const handleReserve = (data) => {
+	isShowtimeHint.value = data
+}
+
+
+const handleChange = () => {
+	showAddStudentHint.value = false
+	uni.navigateTo({
+		url: '/modules/common/student/student-query'
+	})
+}
+
+// 操作指南
+const handleInstructions = () => {
+	const url = `https://zxxwiki.sxidc.com/zh/%E5%B8%AE%E5%8A%A9%E4%B8%AD%E5%BF%83/%E6%96%B0%E6%89%8B%E5%85%A5%E9%97%A8/%E5%AE%B6%E9%95%BF%E5%85%A5%E9%97%A8`
+	uni.navigateTo({
+		url: `/modules/common/web-view?url=${encodeURIComponent(url)}`
+	})
+}
+// 去添加
+const handAddStudent = () => {
+	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
+	showAddStudentHint.value = true
+}
+// 去查看体检报告
+const handReport = () => {
+	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
+	uni.navigateTo({
+		url: `/modules/common/student/student-report-list`
+	})
+}
+// 学生休复学
+const handleProsthetics = () => {
+	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
+	uni.navigateTo({
+		url: `/modules/common/student-prosthetics/index`
+	})
+}
+// 公益活动
+const handActivity = () => {
+	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
+	uni.navigateTo({
+		url: `/modules/activity/activity-list`
+	})
+}
+// 在线学习
+const handleOnlineStudy = () => {
+	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
+	uni.navigateTo({
+		url: `/modules/class/class`
+	})
+}
+
+// 学生详情
+const handStudentDetail = (item) => {
+	uni.navigateTo({
+		url: '/modules/common/student/student-detail?data=' + encodeURIComponent(JSON.stringify({
+			req_data: item,
+			res_data: { exist: true, bind: true, student: item }
+		}))
+	})
+}
+
+</script>
+
+<style lang="scss" scoped>
+::v-deep .fs-grid-item {
+	padding: 10rpx 0 !important;
+}
+</style>

+ 150 - 264
pages/index/index.vue

@@ -1,301 +1,187 @@
 <template>
-	<view class="wrap pr">
-		<view class="w-full pa tw-top-0 tw-left-0 tw-z-0">
-			<image class="w-full" :src="config.ossPathPerfixs + '/index-bg.png'" mode="widthFix"></image>
-		</view>
-		<view
-			class="pa tw-top-[50rpx] tw-right-0 tw-z-20 tw-text-[#333] tw-text-[24rpx] tw-font-bold tw-px-[24rpx] tw-py-[10rpx] shadow tw-bg-[rgba(255,255,255,0.7)]"
-			style="border-radius: 50rpx 0 0 50rpx;" @click="handleInstructions">
-			操作指南
-		</view>
-		<view class="w-full pr tw-z-10 tw-pb-[10rpx]">
-			<view class="tw-p-[30rpx]">
-				<view class="tw-flex tw-items-center tw-justify-start tw-mt-[60rpx]">
-					<fs-avatar size="100rpx" src="/static/images/tool/logo.png"></fs-avatar>
-					<text class="tw-text-[#fff] tw-text-[26rpx] tw-ml-[4rpx]"
-						style="letter-spacing: 1rpx;">太原市中小学学生卫生保健所</text>
+	<view class="wrap" v-if="isValidQrCode">
+		<image class="bg-image" src="/static/images/welcome/eye-examine-bg.png" mode="widthFix"></image>
+		<view class="content">
+			<view class="form-box">
+				<view class="school-name" v-if="schoolName">
+					<text>{{ schoolName }}</text>
 				</view>
+				<van-field v-model="formData.name" label="姓名" placeholder="请输入姓名" required label-align="left" />
+				<van-field v-model="formData.phone" label="电话" placeholder="请输入电话" type="tel" required label-align="left" />
+				<van-field v-model="formData.organization" label="机构" placeholder="请输入机构" required label-align="left" />
 			</view>
-
-			<view class="tw-px-[10rpx]">
-				<fs-notice-bar :list="noticeList" :interval="3000" :showClose="false" :vertical="true"
-					bgColor="rgba(255,255,255,0.8)" color="#333"></fs-notice-bar>
-			</view>
-
-			<view class="tw-px-[30rpx] tw-pb-[30rpx]">
-				<view
-					class="card-shadow tw-mt-[40rpx] tw-p-[40rpx] tw-pb-[70rpx] tw-flex direction-column justify-between align-center pr"
-					style="background-color: rgba(255, 255, 255, 0.6);" v-if="studentList.length === 0">
-					<fs-avatar width="416rpx" height="191rpx" src="/static/images/tool/add-empty.png"></fs-avatar>
-					<view class="sub pa tw-z-10 tw-mt-[100rpx]">
-						添加学生信息查看体检报告
-					</view>
-					<view class="pa tw-z-10 tw-mt-[160rpx]" @click="handAddStudent">
-						<view class="pr">
-							<fs-avatar class="pa tw-top-[-10rpx] tw-left-0" size="100rpx"
-								src="/static/images/icon/add.png"></fs-avatar>
-							<view class="tw-px-[40rpx] tw-py-[12rpx] tw-pl-[90rpx]"
-								style="background: linear-gradient(-76deg, #00EEA8, #0871FF);border-radius: 50rpx;">
-								<text class="tw-text-[#fff] tw-text-[28rpx]">添加学生</text>
-							</view>
-						</view>
-					</view>
-				</view>
-				<view class="card-shadow tw-mt-[40rpx] tw-p-[30rpx]" style="background-color: rgba(255, 255, 255, 0.6);"
-					v-else>
-					<fs-grid :columnNum="4" bgColor="transparent">
-						<fs-grid-item v-for="(item, index) in studentList" :key="index"
-							@click="handStudentDetail(item)">
-							<view class="text-center">
-								<fs-avatar size="120rpx"
-									:src="`/static/images/avatar/${item.gender == '男' ? 'man' : 'girl'}.png`"></fs-avatar>
-								<view class="tw-mt-[10rpx] tw-text-[26rpx] tw-text-[#333] tw-font-bold">{{ item.name }}
-								</view>
-							</view>
-						</fs-grid-item>
-						<fs-grid-item @click="handAddStudent">
-							<view class="text-center">
-								<fs-avatar size="120rpx" src="/static/images/tool/add-avatar.png"></fs-avatar>
-								<view class="tw-mt-[10rpx] tw-text-[26rpx] tw-text-[#999]">添加学生</view>
-							</view>
-						</fs-grid-item>
-					</fs-grid>
-				</view>
-
-				<view class="tw-mt-[30rpx]">
-					<view class="title-line">
-						<text>常用功能</text>
-					</view>
-				</view>
-				<view class="tw-mt-[10rpx]">
-					<fs-grid gutter="0" :columnNum="1" bgColor="transparent">
-						<fs-grid-item @click="handReport">
-							<view
-								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
-								<view class="tw-flex tw-items-center">
-									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
-										src="/static/images/menu/menua1.png"></fs-avatar>
-									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
-										快速查询学生体检报告
-									</view>
-								</view>
-								<fs-avatar size="36rpx" shape="square"
-									src="/static/images/icon/right-arrow.png"></fs-avatar>
-							</view>
-						</fs-grid-item>
-						<fs-grid-item @click="handleProsthetics">
-							<view
-								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
-								<view class="tw-flex tw-items-center">
-									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
-										src="/static/images/menu/menua2.png"></fs-avatar>
-									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
-										一键办理学生休复学
-									</view>
-								</view>
-								<fs-avatar size="36rpx" shape="square"
-									src="/static/images/icon/right-arrow.png"></fs-avatar>
-							</view>
-						</fs-grid-item>
-						<fs-grid-item @click="handActivity">
-							<view
-								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
-								<view class="tw-flex tw-items-center">
-									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
-										src="/static/images/menu/menua3.png"></fs-avatar>
-									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
-										公益健康服务预约
-									</view>
-								</view>
-								<fs-avatar size="36rpx" shape="square"
-									src="/static/images/icon/right-arrow.png"></fs-avatar>
-							</view>
-						</fs-grid-item>
-						<fs-grid-item @click="handleOnlineStudy">
-							<view
-								class="w-full h-full tw-flex tw-justify-between tw-items-center tw-bg-white card-shadow tw-text-left tw-px-[30rpx] tw-py-[20rpx]">
-								<view class="tw-flex tw-items-center">
-									<fs-avatar size="150rpx" shape="square" class="pr tw-top-[8rpx]"
-										src="/static/images/menu/menua4.png"></fs-avatar>
-									<view class="tw-text-[#333] tw-text-[30rpx] tw-font-bold tw-ml-[10rpx] line1">
-										保健E课堂在线学习
-									</view>
-								</view>
-								<fs-avatar size="36rpx" shape="square"
-									src="/static/images/icon/right-arrow.png"></fs-avatar>
-							</view>
-						</fs-grid-item>
-					</fs-grid>
-				</view>
+			<view class="btn-box">
+				<van-button type="primary" block round @click="handleEnter" :disabled="!isFormValid">进入体检</van-button>
 			</view>
 		</view>
 	</view>
-	<addStudentHint v-if="showAddStudentHint" @change="handleChange"></addStudentHint>
-	<timeHint v-if="isShowtimeHint" :time="0" title="系统公告" :text="noticeInfo.content" @change="handleReserve">
-	</timeHint>
+	<view v-else class="error-page">
+		<view class="error-text">当前二维码无效</view>
+	</view>
 </template>
 
 <script setup>
-import config from '@/utils/config'
-import { useUserStore } from '@/stores/user'
-import addStudentHint from '@/business/add-student-hint'
-import timeHint from '@/business/time-hint'
-
-const user = useUserStore()
-const userInfo = computed(() => user.userInfo)
-const openId = computed(() => user.openId)
+import { getSchoolInfo } from '@/services/common'
 
-const showAddStudentHint = ref(false)
-const studentList = ref([])
+const STORAGE_KEY = 'eye_examine_user_info'
+const isValidQrCode = ref(true)
+const schoolName = ref('')
 
-const isShowtimeHint = ref(false)
-const noticeInfo = ref({})
-
-const noticeList = ref([
-	{
-		title: '本年度体检结果开放查询情况总览',
-		url: '/modules/common/notice/notice-school-list',
-	},
-	{
-		title: '本年度体检结果开放查询情况总览',
-		url: '/modules/common/notice/notice-school-list',
-	},
-	{
-		title: '本年度体检结果开放查询情况总览',
-		url: '/modules/common/notice/notice-school-list',
-	},
-])
-
-onLoad(() => {
-	getNoticeData()
+const formData = ref({
+	name: '',
+	phone: '',
+	organization: '',
+	schoolId: ''
 })
 
-onShow(() => {
-	getStudentList()
+const isFormValid = computed(() => {
+	const phoneReg = /^1[3-9]\d{9}$/
+	return formData.value.name && phoneReg.test(formData.value.phone) && formData.value.organization
 })
 
-onPullDownRefresh(async () => {
-	await getNoticeData()
-	await getStudentList()
-	uni.stopPullDownRefresh()
+onLoad(async (options) => {
+	const savedData = uni.getStorageSync(STORAGE_KEY)
+	
+	if (options.schoolId) {
+		formData.value.schoolId = options.schoolId
+	}
+	
+	if (savedData) {
+		const saved = JSON.parse(savedData)
+		formData.value.name = saved.name || ''
+		formData.value.phone = saved.phone || ''
+		formData.value.organization = saved.organization || ''
+		
+		if (options.schoolId && saved.schoolId !== options.schoolId) {
+			formData.value.schoolId = options.schoolId
+		} else if (saved.schoolId) {
+			formData.value.schoolId = saved.schoolId
+		}
+	}
+	
+	if (!formData.value.schoolId) {
+		isValidQrCode.value = false
+		uni.showToast({
+			title: '当前二维码无效',
+			icon: 'none',
+			duration: 3000
+		})
+		return
+	}
+	
+	try {
+		const res = await getSchoolInfo(formData.value.schoolId)
+		if (res && res.code === 200) {
+			schoolName.value = res.data.name || ''
+		}
+	} catch (error) {
+		console.error('获取学校信息失败', error)
+	}
 })
 
-// 获取学生列表
-const getStudentList = () => {
-	return new Promise((resolve) => {
-		if (openId.value) {
-			// getBindStudentList({ openId: openId.value }).then(res => {
-			// 	studentList.value = res.data || []
-			// }).catch().finally(() => {
-			// 	resolve()
-			// })
-			studentList.value = [
-				{
-					name: '李小明',
-					gender: '男',
-					grade: '六年级',
-					class: '1班',
-					avatar: 'https://img2.baidu.com/it/u=3139169729,4118257360&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
-				},{
-					name: '李飞',
-					gender: '男',
-					grade: '六年级',
-					class: '1班',
-					avatar: 'https://img2.baidu.com/it/u=3139169729,4118257360&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
-				}
-			]
-			resolve()
-		}
+const handleEnter = async () => {
+	if (!formData.value.name || !formData.value.phone || !formData.value.organization) {
+		return uni.showToast({
+			title: '请填写完整信息',
+			icon: 'none'
+		})
+	}
+	
+	if (!formData.value.schoolId) {
+		return uni.showToast({
+			title: '缺少学校ID',
+			icon: 'none'
+		})
+	}
+	
+	uni.setStorageSync(STORAGE_KEY, JSON.stringify(formData.value))
+	uni.navigateTo({
+		url: '/pages/index/eye-examine-index'
 	})
 }
+</script>
 
-// 获取公告
-const getNoticeData = () => {
-	return new Promise((resolve) => {
-		// getNotice().then(res => {
-		// 	if (res.success) {
-		// 		noticeInfo.value = res.data || {}
-		// 		if (noticeInfo.value.open) isShowtimeHint.value = true
-		// 	}
-		// }).finally(() => {
-		// 	resolve()
-		// })
-		noticeInfo.value = {
-			title: '2023年体测结果查询',
-			content: '2023年体测结果查询',
-			open: true,
-			url: '/modules/common/notice/notice-school-list',
-		}
-		resolve()
-	})
-}
-const handleReserve = (data) => {
-	isShowtimeHint.value = data
+<style lang="scss" scoped>
+.wrap {
+	width: 100%;
+	min-height: 100vh;
+	position: relative;
+	overflow: hidden;
+	background: linear-gradient(180deg, #4A9FF5 0%, #FFFFFF 100%);
 }
 
+.bg-image {
+	width: 100%;
+	height: 200rpx;
+	display: block;
+}
 
-const handleChange = () => {
-	showAddStudentHint.value = false
-	uni.navigateTo({
-		url: '/modules/common/student/student-query'
-	})
+.content {
+	padding: 0 30rpx;
+	margin-top: -300rpx;
+	position: relative;
+	z-index: 2;
 }
 
-// 操作指南
-const handleInstructions = () => {
-	const url = `https://zxxwiki.sxidc.com/zh/%E5%B8%AE%E5%8A%A9%E4%B8%AD%E5%BF%83/%E6%96%B0%E6%89%8B%E5%85%A5%E9%97%A8/%E5%AE%B6%E9%95%BF%E5%85%A5%E9%97%A8`
-	uni.navigateTo({
-		url: `/modules/common/web-view?url=${encodeURIComponent(url)}`
-	})
+.school-name {
+	text-align: center;
+	font-size: 36rpx;
+	font-weight: bold;
+	font-family: 'SimHei', 'Microsoft YaHei', sans-serif;
+	color: #000;
+	padding: 40rpx 30rpx 30rpx;
+	border-bottom: 1px solid #eee;
+	word-wrap: break-word;
+	word-break: break-all;
+	line-height: 1.5;
 }
-// 去添加
-const handAddStudent = () => {
-	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
-	showAddStudentHint.value = true
+
+.form-box {
+	background: #fff;
+	border-radius: 20rpx;
+	overflow: hidden;
+	margin-bottom: 60rpx;
+	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
 }
-// 去查看体检报告
-const handReport = () => {
-	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
-	uni.navigateTo({
-		url: `/modules/common/student/student-report-list`
-	})
+
+.form-box :deep(.van-cell) {
+	font-size: 36rpx;
+	padding: 30rpx 16px;
+	flex-direction: column;
+	align-items: flex-start;
 }
-// 学生休复学
-const handleProsthetics = () => {
-	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
-	uni.navigateTo({
-		url: `/modules/common/student-prosthetics/index`
-	})
+
+.form-box :deep(.van-field__label) {
+	font-size: 36rpx;
+	margin-bottom: 20rpx;
+	width: 100%;
 }
-// 公益活动
-const handActivity = () => {
-	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
-	uni.navigateTo({
-		url: `/modules/activity/activity-list`
-	})
+
+.form-box :deep(.van-field__control) {
+	font-size: 36rpx;
+	width: 100%;
 }
-// 在线学习
-const handleOnlineStudy = () => {
-	if (!openId.value) return uni.navigateTo({ url: '/pages/index/welcome' })
-	uni.navigateTo({
-		url: `/modules/class/class`
-	})
+
+.btn-box {
+	padding: 0;
 }
 
-// 学生详情
-const handStudentDetail = (item) => {
-	uni.navigateTo({
-		url: '/modules/common/student/student-detail?data=' + encodeURIComponent(JSON.stringify({
-			req_data: item,
-			res_data: { exist: true, bind: true, student: item }
-		}))
-	})
+.btn-box :deep(.van-button) {
+	height: 100rpx;
+	font-size: 36rpx;
 }
 
-</script>
+.error-page {
+	width: 100%;
+	height: 100vh;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	background: #f5f5f5;
+}
 
-<style lang="scss" scoped>
-::v-deep .fs-grid-item {
-	padding: 10rpx 0 !important;
+.error-text {
+	font-size: 32rpx;
+	color: #999;
 }
 </style>

+ 157 - 0
pages/index/select-input-type.vue

@@ -0,0 +1,157 @@
+<template>
+	<view class="page">
+		<view class="bg-gradient"></view>
+		<van-nav-bar title="选择录入类型" left-arrow @click-left="onBack" fixed :z-index="999" />
+		<view class="content">
+			<view class="student-info">
+				<view class="info-row">
+					<text class="label">姓名</text>
+					<text class="value">{{ studentName }}</text>
+				</view>
+				<view class="info-row">
+					<text class="label">身份证号</text>
+					<text class="value">{{ studentIdCard }}</text>
+				</view>
+			</view>
+			<view class="type-list">
+				<view class="type-card" @click="selectType('vision')" v-if="visionStatus === 0">
+					<view class="type-name">视力数据录入</view>
+					<van-icon name="arrow" />
+				</view>
+				<view class="type-card" @click="selectType('refraction')" v-if="refractionStatus === 0">
+					<view class="type-name">屈光度数据录入</view>
+					<van-icon name="arrow" />
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+const studentName = ref('')
+const studentIdCard = ref('')
+const visionStatus = ref(0)
+const refractionStatus = ref(0)
+const params = ref({})
+
+onLoad((options) => {
+	studentName.value = decodeURIComponent(options.studentName || '')
+	studentIdCard.value = decodeURIComponent(options.studentIdCard || '')
+	visionStatus.value = Number(options.visionStatus || 0)
+	refractionStatus.value = Number(options.refractionStatus || 0)
+	
+	params.value = {
+		studentId: options.studentId,
+		studentName: options.studentName,
+		studentGender: options.studentGender,
+		studentIdCard: options.studentIdCard,
+		classId: options.classId,
+		className: options.className,
+		grade: options.grade,
+		visionDataId: options.visionDataId || '',
+		schoolName: options.schoolName,
+		userName: options.userName,
+		userPhone: options.userPhone,
+		userOrg: options.userOrg
+	}
+})
+
+const selectType = (type) => {
+	const url = `/pages/index/vision-input?${Object.keys(params.value).map(k => `${k}=${encodeURIComponent(params.value[k])}`).join('&')}&type=${type}`
+	uni.navigateTo({ url })
+}
+
+const onBack = () => {
+	uni.navigateBack({
+		delta: 1,
+		success: () => {},
+		fail: () => {
+			uni.switchTab({
+				url: '/pages/index/index'
+			})
+		}
+	})
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	min-height: 100vh;
+	position: relative;
+}
+
+.bg-gradient {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: linear-gradient(180deg, #4A9FF5 0%, #FFFFFF 100%);
+	z-index: 0;
+}
+
+.content {
+	padding: 20rpx;
+	padding-top: calc(20rpx + 46px);
+	position: relative;
+	z-index: 1;
+}
+
+.student-info {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 30rpx;
+	margin-bottom: 20rpx;
+}
+
+.info-row {
+	display: flex;
+	align-items: center;
+	font-size: 28rpx;
+	margin-bottom: 20rpx;
+}
+
+.info-row:last-child {
+	margin-bottom: 0;
+}
+
+.label {
+	width: 160rpx;
+	color: #999;
+}
+
+.value {
+	flex: 1;
+	color: #333;
+}
+
+.type-list {
+	display: flex;
+	flex-direction: column;
+	gap: 20rpx;
+}
+
+.type-card {
+	background: #fff;
+	border-radius: 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding: 50rpx 30rpx;
+	box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+	position: relative;
+}
+
+.type-name {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+	flex: 1;
+	text-align: center;
+}
+
+.type-card :deep(.van-icon) {
+	position: absolute;
+	right: 30rpx;
+}
+</style>

+ 325 - 0
pages/index/student-list.vue

@@ -0,0 +1,325 @@
+<template>
+	<view class="page">
+		<van-nav-bar :title="`${grade} ${className}`" left-arrow @click-left="onBack" fixed />
+		<view class="search-bar">
+			<van-search v-model="searchKeyword" placeholder="搜索学生姓名或身份证号" @input="onSearch" />
+			<van-button type="primary" size="small" @click="addStudent" style="width: 120rpx;">新增</van-button>
+		</view>
+		<view class="tab-bar">
+			<van-tabs v-model:active="activeTab" @change="onTabChange">
+				<van-tab title="未录入"></van-tab>
+				<van-tab title="已录入"></van-tab>
+				<van-tab title="全部"></van-tab>
+			</van-tabs>
+		</view>
+		<view class="content">
+			<view class="student-card" v-for="student in studentList" :key="student.id" @click="goToVisionInput(student)">
+				<view class="card-header">
+					<view class="left">
+						<view class="name">{{ student.name }}</view>
+						<image class="gender-icon" :src="student.gender === '男' ? '/static/images/icon/man.png' : '/static/images/icon/woman.png'" mode="aspectFit"></image>
+					</view>
+					<view class="status-tag" :class="student.input_status === '未录入' ? 'warning' : ''">{{ student.input_status }}</view>
+				</view>
+				<view class="card-body">
+					<view class="info-row">
+						<text class="label">身份证号</text>
+						<text class="value">{{ student.id_card }}</text>
+					</view>
+				</view>
+				<view class="divider" v-if="(student.vision_input_user || student.refraction_input_user) && activeTab !== 0"></view>
+				<view class="card-footer" v-if="activeTab !== 0">
+					<view class="info-row" v-if="student.vision_input_user">
+						<text class="label">视力录入医生</text>
+						<text class="value">{{ student.vision_input_user }}</text>
+					</view>
+					<view class="info-row" v-if="student.vision_input_time">
+						<text class="label">视力录入时间</text>
+						<text class="value">{{ student.vision_input_time }}</text>
+					</view>
+					<view class="info-row" v-if="student.refraction_input_user">
+						<text class="label">屈光度录入医生</text>
+						<text class="value">{{ student.refraction_input_user }}</text>
+					</view>
+					<view class="info-row" v-if="student.refraction_input_time">
+						<text class="label">屈光度录入时间</text>
+						<text class="value">{{ student.refraction_input_time }}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { getIncompleteVisionStudents } from '@/services/common'
+
+const studentList = ref([])
+const activeTab = ref(0)
+const classId = ref('')
+const className = ref('')
+const grade = ref('')
+const searchKeyword = ref('')
+const schoolName = ref('')
+const userName = ref('')
+const userPhone = ref('')
+const userOrg = ref('')
+let searchTimer = null
+
+const isEntered = (student) => {
+	return student.vision_input_status === 1 && student.refraction_input_status === 1
+}
+
+const formatTime = (time) => {
+	if (!time) return ''
+	try {
+		const date = new Date(time)
+		if (isNaN(date.getTime())) return time
+		date.setHours(date.getHours() + 8)
+		const year = date.getFullYear()
+		const month = String(date.getMonth() + 1).padStart(2, '0')
+		const day = String(date.getDate()).padStart(2, '0')
+		const hour = String(date.getHours()).padStart(2, '0')
+		const minute = String(date.getMinutes()).padStart(2, '0')
+		const second = String(date.getSeconds()).padStart(2, '0')
+		return `${year}-${month}-${day} ${hour}:${minute}:${second}`
+	} catch (e) {
+		return time
+	}
+}
+
+const onTabChange = () => {
+	fetchStudents()
+}
+
+const fetchStudents = async () => {
+	if (!classId.value) return
+	
+	try {
+		const params = {}
+		if (searchKeyword.value) {
+			if (/^\d+$/.test(searchKeyword.value)) {
+				params.id_card = searchKeyword.value
+			} else {
+				params.name = searchKeyword.value
+			}
+		}
+		
+		if (activeTab.value === 0) {
+			params.status = 'incomplete'
+		} else if (activeTab.value === 1) {
+			params.status = 'complete'
+		} else if (activeTab.value === 2) {
+			params.status = 'all'
+		}
+		
+		const res = await getIncompleteVisionStudents(classId.value, params)
+		if (res && res.code === 200) {
+			studentList.value = res.data.list || []
+		}
+	} catch (error) {
+		console.error('获取学生列表失败', error)
+	}
+}
+
+const onSearch = () => {
+	if (searchTimer) clearTimeout(searchTimer)
+	searchTimer = setTimeout(() => {
+		fetchStudents()
+	}, 500)
+}
+
+onLoad(async (options) => {
+	classId.value = options.classId || ''
+	className.value = options.className || ''
+	grade.value = options.grade || ''
+	schoolName.value = decodeURIComponent(options.schoolName || '')
+	userName.value = decodeURIComponent(options.userName || '')
+	userPhone.value = decodeURIComponent(options.userPhone || '')
+	userOrg.value = decodeURIComponent(options.userOrg || '')
+	
+	if (!classId.value) {
+		uni.showToast({
+			title: '班级信息缺失',
+			icon: 'none'
+		})
+		return
+	}
+	
+	fetchStudents()
+})
+
+onShow(() => {
+	if (classId.value) {
+		fetchStudents()
+	}
+})
+
+const addStudent = () => {
+	uni.navigateTo({
+		url: `/pages/index/add-student?classId=${classId.value}&userName=${encodeURIComponent(userName.value)}&userPhone=${encodeURIComponent(userPhone.value)}`
+	})
+}
+
+const onBack = () => {
+	uni.navigateBack()
+}
+
+const goToVisionInput = (student) => {
+	const params = {
+		studentId: student.id,
+		studentName: student.name,
+		studentGender: student.gender,
+		studentIdCard: student.id_card,
+		classId: classId.value,
+		className: className.value,
+		grade: grade.value,
+		visionDataId: student.vision_data_id || '',
+		schoolName: schoolName.value,
+		userName: userName.value,
+		userPhone: userPhone.value,
+		userOrg: userOrg.value,
+		visionStatus: student.vision_input_status,
+		refractionStatus: student.refraction_input_status
+	}
+	
+	const visionStatus = student.vision_input_status
+	const refractionStatus = student.refraction_input_status
+	
+	if (visionStatus === 1 && refractionStatus === 1) {
+		return uni.navigateTo({
+			url: `/pages/index/vision-detail?${Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')}`
+		})
+	}
+	
+	uni.navigateTo({
+		url: `/pages/index/select-input-type?${Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')}`
+	})
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	min-height: 100vh;
+	background: #f5f5f5;
+}
+
+.content {
+	padding: 20rpx;
+	padding-top: calc(20rpx + 46px + 54px + 44px);
+}
+
+.tab-bar {
+	position: fixed;
+	top: calc(46px + 54px);
+	left: 0;
+	right: 0;
+	z-index: 999;
+	background: #fff;
+}
+
+.search-bar {
+	position: fixed;
+	top: 46px;
+	left: 0;
+	right: 0;
+	z-index: 999;
+	background: #fff;
+	display: flex;
+	align-items: center;
+	padding: 10rpx 20rpx;
+	gap: 10rpx;
+}
+
+.search-bar :deep(.van-search) {
+	flex: 1;
+	padding: 0;
+}
+
+.search-bar :deep(.van-button) {
+	flex-shrink: 0;
+}
+
+.student-card {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 30rpx;
+	margin-bottom: 20rpx;
+	box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.card-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 20rpx;
+	padding-bottom: 20rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+}
+
+.left {
+	display: flex;
+	align-items: center;
+	gap: 20rpx;
+}
+
+.name {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.gender-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.status-tag {
+	padding: 8rpx 20rpx;
+	border-radius: 30rpx;
+	font-size: 24rpx;
+	background: #e8f8f5;
+	color: #07c160;
+}
+
+.status-tag.warning {
+	background: #fff3e8;
+	color: #ff976a;
+}
+
+.card-body {
+	padding-top: 10rpx;
+}
+
+.divider {
+	height: 1rpx;
+	background: #f0f0f0;
+	margin: 20rpx 0;
+}
+
+.card-footer {
+	padding-top: 10rpx;
+}
+
+.info-row {
+	display: flex;
+	align-items: center;
+	font-size: 26rpx;
+	color: #666;
+	margin-bottom: 10rpx;
+	justify-content: space-between;
+}
+
+.info-row:last-child {
+	margin-bottom: 0;
+}
+
+.label {
+	color: #999;
+}
+
+.value {
+	color: #333;
+	text-align: right;
+}
+</style>

+ 271 - 0
pages/index/vision-detail.vue

@@ -0,0 +1,271 @@
+<template>
+	<view class="page">
+		<view class="bg-gradient"></view>
+		<van-nav-bar title="视力数据详情" left-arrow @click-left="onBack" fixed :z-index="999" />
+		<view class="header">
+			<view class="student-info">
+				<view class="info-row">
+					<image class="gender-icon" :src="studentInfo.gender === '男' ? '/static/images/icon/man.png' : '/static/images/icon/woman.png'" mode="aspectFit"></image>
+					<text class="student-name">{{ studentInfo.name }}</text>
+				</view>
+				<view class="detail-row">
+					<text>学校:{{ schoolName }}</text>
+				</view>
+				<view class="detail-row">
+					<text>班级:{{ studentInfo.class }}</text>
+				</view>
+				<view class="detail-row">
+					<text>身份证:{{ studentInfo.idCard }}</text>
+				</view>
+			</view>
+		</view>
+
+		<view class="content">
+			<view class="section-header">
+				<text class="section-title">视力数据</text>
+			</view>
+
+			<view class="form-item">
+				<text class="label">左眼远视力</text>
+				<input class="value-input" v-model="formData.leftVision" type="digit" placeholder="请输入" @blur="formatVision('leftVision')" />
+			</view>
+
+			<view class="form-item">
+				<text class="label">右眼远视力</text>
+				<input class="value-input" v-model="formData.rightVision" type="digit" placeholder="请输入" @blur="formatVision('rightVision')" />
+			</view>
+
+			<view class="section-header" style="margin-top: 40rpx;">
+				<text class="section-title">屈光度数据</text>
+			</view>
+
+			<view class="form-item">
+				<text class="label">左眼屈光度</text>
+				<input class="value-input" v-model="formData.leftRefraction" type="number" placeholder="请输入" />
+			</view>
+
+			<view class="form-item">
+				<text class="label">右眼屈光度</text>
+				<input class="value-input" v-model="formData.rightRefraction" type="number" placeholder="请输入" />
+			</view>
+		</view>
+
+		<view class="footer">
+			<view class="footer-btns">
+				<van-button plain type="primary" round size="large" @click="onBack" icon="replay">返回</van-button>
+				<van-button type="primary" round size="large" icon="success" @click="handleSave">保存</van-button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { getStudentVisionData, updateVisionData } from '@/services/common'
+
+const studentInfo = ref({})
+const schoolName = ref('')
+const visionDataId = ref('')
+const userName = ref('')
+const userPhone = ref('')
+
+const formData = ref({
+	leftVision: '',
+	rightVision: '',
+	leftRefraction: '',
+	rightRefraction: ''
+})
+
+onLoad(async (options) => {
+	studentInfo.value = {
+		id: options.studentId || '',
+		name: decodeURIComponent(options.studentName || ''),
+		gender: decodeURIComponent(options.studentGender || ''),
+		idCard: decodeURIComponent(options.studentIdCard || ''),
+		class: `${decodeURIComponent(options.grade || '')} ${decodeURIComponent(options.className || '')}`
+	}
+	
+	schoolName.value = decodeURIComponent(options.schoolName || '')
+	userName.value = decodeURIComponent(options.userName || '')
+	userPhone.value = decodeURIComponent(options.userPhone || '')
+	
+	await fetchVisionData()
+})
+
+const fetchVisionData = async () => {
+	try {
+		const res = await getStudentVisionData(studentInfo.value.id)
+		if (res && res.code === 200 && res.data) {
+			const data = res.data
+			visionDataId.value = data.id || ''
+			formData.value.leftVision = data.left_eye_vision !== null && data.left_eye_vision !== undefined ? data.left_eye_vision : ''
+			formData.value.rightVision = data.right_eye_vision !== null && data.right_eye_vision !== undefined ? data.right_eye_vision : ''
+			formData.value.leftRefraction = data.left_eye_refraction !== null && data.left_eye_refraction !== undefined ? data.left_eye_refraction : ''
+			formData.value.rightRefraction = data.right_eye_refraction !== null && data.right_eye_refraction !== undefined ? data.right_eye_refraction : ''
+		}
+	} catch (error) {
+		console.error('获取视力数据失败', error)
+	}
+}
+
+const formatVision = (field) => {
+	const value = parseFloat(formData.value[field])
+	if (!isNaN(value)) {
+		formData.value[field] = value.toFixed(1)
+	}
+}
+
+const handleSave = async () => {
+	if (!formData.value.leftVision || !formData.value.rightVision || !formData.value.leftRefraction || !formData.value.rightRefraction) {
+		return uni.showToast({
+			title: '请填写完整信息',
+			icon: 'none'
+		})
+	}
+	
+	try {
+		const params = {
+			left_eye_vision: parseFloat(formData.value.leftVision),
+			right_eye_vision: parseFloat(formData.value.rightVision),
+			left_eye_refraction: parseFloat(formData.value.leftRefraction),
+			right_eye_refraction: parseFloat(formData.value.rightRefraction),
+			input_user: userName.value,
+			operator_phone: userPhone.value
+		}
+		
+		const res = await updateVisionData(visionDataId.value, params)
+		if (res && res.code === 200) {
+			uni.showToast({
+				title: '保存成功',
+				icon: 'success',
+				duration: 1000
+			})
+			setTimeout(() => {
+				uni.navigateBack()
+			}, 1000)
+		}
+	} catch (error) {
+		console.error('保存失败', error)
+	}
+}
+
+const onBack = () => {
+	uni.navigateBack()
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	min-height: 100vh;
+	position: relative;
+}
+
+.bg-gradient {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: linear-gradient(180deg, #4A9FF5 0%, #E8F5FF 100%);
+	z-index: 0;
+	pointer-events: none;
+}
+
+.header {
+	padding: 20rpx 30rpx;
+	padding-top: calc(20rpx + 46px);
+	position: relative;
+	z-index: 1;
+}
+
+.student-info {
+	background: rgba(255, 255, 255, 0.9);
+	border-radius: 20rpx;
+	padding: 30rpx;
+}
+
+.info-row {
+	display: flex;
+	align-items: center;
+	margin-bottom: 20rpx;
+}
+
+.gender-icon {
+	width: 40rpx;
+	height: 40rpx;
+	margin-right: 10rpx;
+}
+
+.student-name {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.detail-row {
+	font-size: 26rpx;
+	color: #666;
+	margin-top: 10rpx;
+}
+
+.content {
+	padding: 30rpx;
+	position: relative;
+	z-index: 1;
+	padding-bottom: 150rpx;
+}
+
+.section-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 20rpx;
+}
+
+.section-title {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.form-item {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 30rpx;
+	margin-bottom: 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.label {
+	font-size: 28rpx;
+	color: #333;
+}
+
+.value-input {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #0063F5;
+	flex: 1;
+	text-align: center;
+}
+
+.footer {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	padding: 30rpx;
+	background: #fff;
+	z-index: 10;
+}
+
+.footer-btns {
+	display: flex;
+	gap: 20rpx;
+	
+	button {
+		flex: 1;
+	}
+}
+</style>

+ 312 - 0
pages/index/vision-input.vue

@@ -0,0 +1,312 @@
+<template>
+	<view class="page">
+		<view class="bg-gradient"></view>
+		<van-nav-bar :title="inputType === 'vision' ? '视力录入' : '屈光度录入'" left-arrow @click-left="onBack" fixed :z-index="999" />
+		<view class="header">
+			<view class="student-info">
+				<view class="info-row">
+					<image class="gender-icon" :src="studentInfo.gender === '男' ? '/static/images/icon/man.png' : '/static/images/icon/woman.png'" mode="aspectFit"></image>
+					<text class="student-name">{{ studentInfo.name }}</text>
+				</view>
+				<view class="detail-row">
+					<text>学校:{{ schoolName }}</text>
+				</view>
+				<view class="detail-row">
+					<text>班级:{{ studentInfo.class }}</text>
+				</view>
+				<view class="detail-row">
+					<text>身份证:{{ studentInfo.idCard }}</text>
+				</view>
+			</view>
+		</view>
+
+		<view class="content">
+			<view class="section-header">
+				<text class="section-title">眼科检查</text>
+			</view>
+
+			<view class="form-item" v-if="inputType === 'vision'">
+				<text class="label">左眼远视力</text>
+				<input class="value-input" v-model="formData.leftVision" type="digit" placeholder="请输入" @blur="formatVision('leftVision')" />
+			</view>
+
+			<view class="form-item" v-if="inputType === 'vision'">
+				<text class="label">右眼远视力</text>
+				<input class="value-input" v-model="formData.rightVision" type="digit" placeholder="请输入" @blur="formatVision('rightVision')" />
+			</view>
+
+			<view class="form-item" v-if="inputType === 'refraction'">
+				<text class="label">左眼屈光度</text>
+				<input class="value-input" v-model="formData.leftRefraction" type="number" placeholder="请输入" />
+			</view>
+
+			<view class="form-item" v-if="inputType === 'refraction'">
+				<text class="label">右眼屈光度</text>
+				<input class="value-input" v-model="formData.rightRefraction" type="number" placeholder="请输入" />
+			</view>
+		</view>
+
+		<view class="footer">
+			<view class="footer-btns">
+				<van-button plain type="primary" round size="large" @click="onBack" icon="replay">返回</van-button>
+				<van-button type="primary" round size="large" icon="success" @click="handleSave">保存</van-button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+import { inputVisionData, inputRefractionData } from '@/services/common'
+
+const STORAGE_KEY = 'eye_examine_user_info'
+const userInfo = ref({})
+const studentInfo = ref({})
+const inputType = ref('')
+const visionDataId = ref('')
+const schoolName = ref('')
+
+const formData = ref({
+	leftVision: '',
+	rightVision: '',
+	leftRefraction: '',
+	rightRefraction: ''
+})
+
+onLoad((options) => {
+	studentInfo.value = {
+		id: options.studentId || '',
+		name: decodeURIComponent(options.studentName || ''),
+		gender: decodeURIComponent(options.studentGender || ''),
+		idCard: decodeURIComponent(options.studentIdCard || ''),
+		classId: options.classId || '',
+		class: `${decodeURIComponent(options.grade || '')} ${decodeURIComponent(options.className || '')}`
+	}
+	
+	inputType.value = options.type || ''
+	visionDataId.value = options.visionDataId || ''
+	schoolName.value = decodeURIComponent(options.schoolName || '')
+	
+	userInfo.value = {
+		name: decodeURIComponent(options.userName || ''),
+		phone: decodeURIComponent(options.userPhone || ''),
+		organization: decodeURIComponent(options.userOrg || '')
+	}
+})
+
+const onBack = () => {
+	uni.navigateBack({
+		delta: 1
+	})
+}
+
+const formatVision = (field) => {
+	const value = parseFloat(formData.value[field])
+	if (!isNaN(value)) {
+		formData.value[field] = value.toFixed(1)
+	}
+}
+
+const handleSave = async () => {
+	if (inputType.value === 'vision') {
+		if (!formData.value.leftVision || !formData.value.rightVision) {
+			return uni.showToast({
+				title: '请填写完整视力信息',
+				icon: 'none'
+			})
+		}
+		
+		try {
+			const params = {
+				student_id: parseInt(studentInfo.value.id),
+				left_eye_vision: parseFloat(formData.value.leftVision),
+				right_eye_vision: parseFloat(formData.value.rightVision),
+				input_user: userInfo.value.name,
+				operator_phone: userInfo.value.phone
+			}
+			
+			if (visionDataId.value) {
+				params.vision_data_id = visionDataId.value
+			}
+			
+			const res = await inputVisionData(params)
+			if (res && res.code === 200) {
+				uni.showToast({
+					title: '保存成功',
+					icon: 'success',
+					duration: 1000
+				})
+				setTimeout(() => {
+					uni.navigateBack({ delta: 2 })
+				}, 1000)
+			}
+		} catch (error) {
+			console.error('保存视力数据失败', error)
+		}
+	} else if (inputType.value === 'refraction') {
+		if (!formData.value.leftRefraction || !formData.value.rightRefraction) {
+			return uni.showToast({
+				title: '请填写完整屈光度信息',
+				icon: 'none'
+			})
+		}
+		
+		try {
+			const params = {
+				student_id: parseInt(studentInfo.value.id),
+				left_eye_refraction: parseFloat(formData.value.leftRefraction),
+				right_eye_refraction: parseFloat(formData.value.rightRefraction),
+				input_user: userInfo.value.name,
+				operator_phone: userInfo.value.phone
+			}
+			
+			if (visionDataId.value) {
+				params.vision_data_id = visionDataId.value
+			}
+			
+			const res = await inputRefractionData(params)
+			if (res && res.code === 200) {
+				uni.showToast({
+					title: '保存成功',
+					icon: 'success',
+					duration: 1000
+				})
+				setTimeout(() => {
+					uni.navigateBack({ delta: 2 })
+				}, 1000)
+			}
+		} catch (error) {
+			console.error('保存屈光度数据失败', error)
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.page {
+	min-height: 100vh;
+	position: relative;
+}
+
+.bg-gradient {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background: linear-gradient(180deg, #4A9FF5 0%, #E8F5FF 100%);
+	z-index: 0;
+	pointer-events: none;
+}
+
+.header {
+	padding: 20rpx 30rpx;
+	padding-top: calc(20rpx + 46px);
+	position: relative;
+	z-index: 1;
+}
+
+.student-info {
+	background: rgba(255, 255, 255, 0.9);
+	border-radius: 20rpx;
+	padding: 30rpx;
+}
+
+.info-row {
+	display: flex;
+	align-items: center;
+	margin-bottom: 20rpx;
+}
+
+.gender-icon {
+	width: 40rpx;
+	height: 40rpx;
+	margin-right: 10rpx;
+}
+
+.student-name {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.detail-row {
+	font-size: 26rpx;
+	color: #666;
+	margin-top: 10rpx;
+}
+
+.content {
+	padding: 30rpx;
+	position: relative;
+	z-index: 1;
+	padding-bottom: 150rpx;
+}
+
+.section-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 20rpx;
+}
+
+.section-title {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #333;
+}
+
+.form-item {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 30rpx;
+	margin-bottom: 20rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+
+.label {
+	font-size: 28rpx;
+	color: #333;
+}
+
+.value {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #0063F5;
+	flex: 1;
+	text-align: center;
+}
+
+.value-input {
+	font-size: 32rpx;
+	font-weight: bold;
+	color: #0063F5;
+	flex: 1;
+	text-align: center;
+}
+
+.edit-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.footer {
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	padding: 30rpx;
+	background: #fff;
+	z-index: 10;
+}
+
+.footer-btns {
+	display: flex;
+	gap: 20rpx;
+	
+	button {
+		flex: 1;
+	}
+}
+</style>

+ 2 - 3
pages/index/welcome.vue

@@ -54,10 +54,9 @@ const tautology = async () => {
 		const sharePath = shareParamsObj.sharePath || ''
 		if (sharePath) {
 			const data = { url: `${sharePath}${params}` }
-			if (sharePath.split('/').filter(item => item)[0] == 'pages') uni.switchTab(data)
-			else uni.redirectTo(data)
+			uni.redirectTo(data)
 		} else {
-			uni.switchTab({
+			uni.redirectTo({
 				url: `/pages/index/index`
 			})
 		}

+ 191 - 0
pnpm-lock.yaml

@@ -14,6 +14,9 @@ importers:
       dayjs:
         specifier: ^1.10.6
         version: 1.11.5
+      vant:
+        specifier: ^4.9.22
+        version: 4.9.22(vue@3.5.30)
     devDependencies:
       tailwindcss:
         specifier: ^3.4.15
@@ -52,10 +55,18 @@ packages:
     resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-string-parser@7.27.1':
+    resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-validator-identifier@7.25.9':
     resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-validator-identifier@7.28.5':
+    resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/parser@7.26.10':
     resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==}
     engines: {node: '>=6.0.0'}
@@ -66,6 +77,11 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
+  '@babel/parser@7.29.0':
+    resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
   '@babel/template@7.27.0':
     resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==}
     engines: {node: '>=6.9.0'}
@@ -82,6 +98,10 @@ packages:
     resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/types@7.29.0':
+    resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+    engines: {node: '>=6.9.0'}
+
   '@csstools/postcss-is-pseudo-class@5.0.1':
     resolution: {integrity: sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==}
     engines: {node: '>=18'}
@@ -120,6 +140,9 @@ packages:
   '@jridgewell/sourcemap-codec@1.5.0':
     resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
 
+  '@jridgewell/sourcemap-codec@1.5.5':
+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
@@ -169,6 +192,43 @@ packages:
   '@types/estree@1.0.1':
     resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
 
+  '@vant/popperjs@1.3.0':
+    resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==}
+
+  '@vant/use@1.6.0':
+    resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==}
+    peerDependencies:
+      vue: ^3.0.0
+
+  '@vue/compiler-core@3.5.30':
+    resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==}
+
+  '@vue/compiler-dom@3.5.30':
+    resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==}
+
+  '@vue/compiler-sfc@3.5.30':
+    resolution: {integrity: sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==}
+
+  '@vue/compiler-ssr@3.5.30':
+    resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==}
+
+  '@vue/reactivity@3.5.30':
+    resolution: {integrity: sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==}
+
+  '@vue/runtime-core@3.5.30':
+    resolution: {integrity: sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==}
+
+  '@vue/runtime-dom@3.5.30':
+    resolution: {integrity: sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==}
+
+  '@vue/server-renderer@3.5.30':
+    resolution: {integrity: sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==}
+    peerDependencies:
+      vue: 3.5.30
+
+  '@vue/shared@3.5.30':
+    resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==}
+
   '@weapp-core/escape@2.1.0':
     resolution: {integrity: sha512-nloTsMWy6nIT5X8HYTaqIlU1ILLmQbM+n5XwGPzLEtkGCTtpCUsjobRSjkuWiozOaFyoM7SZkwQETF9syCD55g==}
 
@@ -315,6 +375,9 @@ packages:
     engines: {node: '>=4'}
     hasBin: true
 
+  csstype@3.2.3:
+    resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
   dayjs@1.11.5:
     resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
 
@@ -376,6 +439,10 @@ packages:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
 
+  entities@7.0.1:
+    resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
+    engines: {node: '>=0.12'}
+
   err-code@2.0.3:
     resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
 
@@ -577,6 +644,9 @@ packages:
   magic-string@0.30.12:
     resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
 
+  magic-string@0.30.21:
+    resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
   magic-string@0.30.3:
     resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==}
     engines: {node: '>=12'}
@@ -814,6 +884,10 @@ packages:
     resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
     engines: {node: ^10 || ^12 || >=14}
 
+  postcss@8.5.8:
+    resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
+    engines: {node: ^10 || ^12 || >=14}
+
   proc-log@5.0.0:
     resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
     engines: {node: ^18.17.0 || >=20.5.0}
@@ -1020,6 +1094,19 @@ packages:
     resolution: {integrity: sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==}
     engines: {node: ^18.17.0 || >=20.5.0}
 
+  vant@4.9.22:
+    resolution: {integrity: sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==}
+    peerDependencies:
+      vue: ^3.0.0
+
+  vue@3.5.30:
+    resolution: {integrity: sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   weapp-tailwindcss@3.7.0:
     resolution: {integrity: sha512-JM1wyRbGQHnXmnmetGzP0ygQR5Yy2gGDNdRrM1lXgC+5pNBw5f6W8i8RvxVGOe4MNuWljey0X34qtOSnw2+9Lw==}
     engines: {node: ^18.17.0 || >=20.5.0}
@@ -1081,8 +1168,12 @@ snapshots:
 
   '@babel/helper-string-parser@7.25.9': {}
 
+  '@babel/helper-string-parser@7.27.1': {}
+
   '@babel/helper-validator-identifier@7.25.9': {}
 
+  '@babel/helper-validator-identifier@7.28.5': {}
+
   '@babel/parser@7.26.10':
     dependencies:
       '@babel/types': 7.26.10
@@ -1091,6 +1182,10 @@ snapshots:
     dependencies:
       '@babel/types': 7.27.0
 
+  '@babel/parser@7.29.0':
+    dependencies:
+      '@babel/types': 7.29.0
+
   '@babel/template@7.27.0':
     dependencies:
       '@babel/code-frame': 7.26.2
@@ -1119,6 +1214,11 @@ snapshots:
       '@babel/helper-string-parser': 7.25.9
       '@babel/helper-validator-identifier': 7.25.9
 
+  '@babel/types@7.29.0':
+    dependencies:
+      '@babel/helper-string-parser': 7.27.1
+      '@babel/helper-validator-identifier': 7.28.5
+
   '@csstools/postcss-is-pseudo-class@5.0.1(postcss@8.4.49)':
     dependencies:
       '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0)
@@ -1156,6 +1256,8 @@ snapshots:
 
   '@jridgewell/sourcemap-codec@1.5.0': {}
 
+  '@jridgewell/sourcemap-codec@1.5.5': {}
+
   '@jridgewell/trace-mapping@0.3.25':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
@@ -1212,6 +1314,66 @@ snapshots:
 
   '@types/estree@1.0.1': {}
 
+  '@vant/popperjs@1.3.0': {}
+
+  '@vant/use@1.6.0(vue@3.5.30)':
+    dependencies:
+      vue: 3.5.30
+
+  '@vue/compiler-core@3.5.30':
+    dependencies:
+      '@babel/parser': 7.29.0
+      '@vue/shared': 3.5.30
+      entities: 7.0.1
+      estree-walker: 2.0.2
+      source-map-js: 1.2.1
+
+  '@vue/compiler-dom@3.5.30':
+    dependencies:
+      '@vue/compiler-core': 3.5.30
+      '@vue/shared': 3.5.30
+
+  '@vue/compiler-sfc@3.5.30':
+    dependencies:
+      '@babel/parser': 7.29.0
+      '@vue/compiler-core': 3.5.30
+      '@vue/compiler-dom': 3.5.30
+      '@vue/compiler-ssr': 3.5.30
+      '@vue/shared': 3.5.30
+      estree-walker: 2.0.2
+      magic-string: 0.30.21
+      postcss: 8.5.8
+      source-map-js: 1.2.1
+
+  '@vue/compiler-ssr@3.5.30':
+    dependencies:
+      '@vue/compiler-dom': 3.5.30
+      '@vue/shared': 3.5.30
+
+  '@vue/reactivity@3.5.30':
+    dependencies:
+      '@vue/shared': 3.5.30
+
+  '@vue/runtime-core@3.5.30':
+    dependencies:
+      '@vue/reactivity': 3.5.30
+      '@vue/shared': 3.5.30
+
+  '@vue/runtime-dom@3.5.30':
+    dependencies:
+      '@vue/reactivity': 3.5.30
+      '@vue/runtime-core': 3.5.30
+      '@vue/shared': 3.5.30
+      csstype: 3.2.3
+
+  '@vue/server-renderer@3.5.30(vue@3.5.30)':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.30
+      '@vue/shared': 3.5.30
+      vue: 3.5.30
+
+  '@vue/shared@3.5.30': {}
+
   '@weapp-core/escape@2.1.0': {}
 
   '@weapp-core/regex@1.0.1': {}
@@ -1355,6 +1517,8 @@ snapshots:
 
   cssesc@3.0.0: {}
 
+  csstype@3.2.3: {}
+
   dayjs@1.11.5: {}
 
   debug@4.3.7:
@@ -1404,6 +1568,8 @@ snapshots:
 
   entities@4.5.0: {}
 
+  entities@7.0.1: {}
+
   err-code@2.0.3: {}
 
   escape-string-regexp@5.0.0: {}
@@ -1606,6 +1772,10 @@ snapshots:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  magic-string@0.30.21:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+
   magic-string@0.30.3:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.4.15
@@ -1854,6 +2024,12 @@ snapshots:
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
+  postcss@8.5.8:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
   proc-log@5.0.0: {}
 
   promise-retry@2.0.1:
@@ -2111,6 +2287,21 @@ snapshots:
 
   validate-npm-package-name@6.0.0: {}
 
+  vant@4.9.22(vue@3.5.30):
+    dependencies:
+      '@vant/popperjs': 1.3.0
+      '@vant/use': 1.6.0(vue@3.5.30)
+      '@vue/shared': 3.5.30
+      vue: 3.5.30
+
+  vue@3.5.30:
+    dependencies:
+      '@vue/compiler-dom': 3.5.30
+      '@vue/compiler-sfc': 3.5.30
+      '@vue/runtime-dom': 3.5.30
+      '@vue/server-renderer': 3.5.30(vue@3.5.30)
+      '@vue/shared': 3.5.30
+
   weapp-tailwindcss@3.7.0(tailwindcss@3.4.17):
     dependencies:
       '@ast-core/escape': 1.0.1

+ 35 - 1
services/common.js

@@ -82,7 +82,17 @@ export function miniAppLogin() {
 }
 
 
-// 添加意见反馈
+export function getSchoolInfo(schoolId) {
+	return http.get(`schools/${schoolId}`, {}, { isAuth: false })
+}
+
+export function getSchoolGrades(schoolId) {
+	return http.get(`schools/${schoolId}/grades`, {}, { isAuth: false })
+}
+
+export function getGradeClasses(schoolId, grade) {
+	return http.get(`schools/${schoolId}/grades-with-classes`, { grade }, { isAuth: false })
+}
 export function addAdvice(data) {
 	return http.post('user/advice', data)
 }
@@ -101,3 +111,27 @@ export function getPolicy() {
 export function getAgreement() {
 	return http.get('user/userServiceAgreement')
 }
+
+export function addStudent(data) {
+	return http.post('students', data, { isAuth: false })
+}
+
+export function getIncompleteVisionStudents(classId, params = {}) {
+	return http.get(`classes/${classId}/incomplete-vision-students`, params, { isAuth: false })
+}
+
+export function inputVisionData(data) {
+	return http.post('vision/input-vision', data, { isAuth: false })
+}
+
+export function inputRefractionData(data) {
+	return http.post('vision/input-refraction', data, { isAuth: false })
+}
+
+export function getStudentVisionData(studentId) {
+	return http.get(`students/${studentId}/vision-data`, {}, { isAuth: false })
+}
+
+export function updateVisionData(visionDataId, data) {
+	return http.put(`vision-data/${visionDataId}`, data, { isAuth: false })
+}

BIN
static/images/welcome/eye-examine-bg.png


+ 5 - 4
utils/config.js

@@ -1,11 +1,11 @@
-// const baseUrl = process.env.NODE_ENV === 'development' ? 'http://10.0.0.180:8888/' : 'https://zxxapp.sxidc.com/'
-const baseUrl = process.env.NODE_ENV === 'development' ? 'http://10.0.0.8:30700/' : 'http://10.0.0.151:30700/'
+// const baseUrl = process.env.NODE_ENV === 'development' ? 'http://10.0.0.85:8080/' : 'http://10.0.0.85:8080/'
+const baseUrl = process.env.NODE_ENV === 'development' ? 'http://fs.towantto.com:20201/' : 'http://fs.towantto.com:20201/'
 
 const ossPathPerfixs = 'https://fskj-res.oss-cn-zhangjiakou.aliyuncs.com/zxx/images'
 
 const config = {
 	baseUrl,
-	apiBaseUrl: baseUrl + 'mbwcb/api',
+	apiBaseUrl: baseUrl,
 	imgBaseUrl: baseUrl + '',
 	uploadUrl: '/upload',
 	httpDefaultOption: {
@@ -16,6 +16,7 @@ const config = {
 	ossPathPerfixs,
 	defaultAvatarUrl: '/static/images/tool/default-avatar.png',
 	defaultErrorImg: '/static/images/tool/defaultErrorImg.png',
+	schoolId: '032bd55dc7984195b8e492a713007bb3'
 }
 
-export default config
+export default config

+ 3 - 3
utils/http.js

@@ -19,7 +19,7 @@ const request = (method, url, data, opt) => {
 		data
 	}).then(res => {
 		const { statusCode, data } = res
-		const { code, errCode, msg, success } = data || {}
+		const { code, errCode, msg, message, success } = data || {}
 		if (statusCode === 401 && [100002, 100003].includes(code)) {
 			uni.showToast({
 				title: '登录已过期,即将重新登录...',
@@ -37,9 +37,9 @@ const request = (method, url, data, opt) => {
 			}, 1500);
 			return data
 		} else {
-			if (!success) {
+			if (code !== 200 && success !== true) {
 				uni.showToast({
-					title: msg || '请求失败',
+					title: message || msg || '请求失败',
 					icon: 'none'
 				})
 			}

+ 0 - 2
vite.config.js

@@ -22,7 +22,6 @@ export default defineConfig({
 		uvwt({
 			rem2rpx: true,
 			disabled: WeappTailwindcssDisabled,
-			// 由于 hbuilderx 会改变 process.cwd 所以这里必须传入当前目录的绝对路径
 			tailwindcssBasedir: __dirname
 		})
 	],
@@ -30,7 +29,6 @@ export default defineConfig({
 		postcss: {
 			plugins: [
 				require("tailwindcss")({
-					// 注意此处,手动传入你 `tailwind.config.js` 的绝对路径
 					config: resolve("./tailwind.config.js")
 				}),
 				require("autoprefixer")

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