From a0e5c9a024a2f1f0e1dd6540d86dfabb72a865a6 Mon Sep 17 00:00:00 2001
From: zlwu <704541015@qq.com>
Date: Fri, 24 Oct 2025 22:20:29 +0800
Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
mobile/package.json | 1 +
mobile/src/api/system/dict/data.js | 35 +
mobile/src/common/README-API调用规范.md | 413 +++
mobile/src/common/config.js | 4 +-
mobile/src/pages.json | 55 +-
.../src/pages/xgss/wsjc/components/README.md | 210 ++
.../xgss/wsjc/components/SingleRoomCheck.vue | 2212 +++++++++++++++++
.../wsjc/components/ViolationRecordDialog.vue | 1901 ++++++++++++++
mobile/src/pages/xgss/wsjc/wsjc.vue | 2 +-
mobile/src/pages/xgss/wsjc/wsjcMainAdd.vue | 971 ++++++++
mobile/src/utils/upload.js | 308 +++
11 files changed, 6086 insertions(+), 26 deletions(-)
create mode 100644 mobile/src/api/system/dict/data.js
create mode 100644 mobile/src/common/README-API调用规范.md
create mode 100644 mobile/src/pages/xgss/wsjc/components/README.md
create mode 100644 mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue
create mode 100644 mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue
create mode 100644 mobile/src/pages/xgss/wsjc/wsjcMainAdd.vue
create mode 100644 mobile/src/utils/upload.js
diff --git a/mobile/package.json b/mobile/package.json
index eedf7efa..b2d21a2a 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -73,6 +73,7 @@
"@vue/shared": "^3.0.0",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",
+ "dayjs": "^1.11.18",
"flyio": "^0.6.2",
"jsencrypt": "^3.3.2",
"uview-ui": "2.0.38",
diff --git a/mobile/src/api/system/dict/data.js b/mobile/src/api/system/dict/data.js
new file mode 100644
index 00000000..8a4b10e1
--- /dev/null
+++ b/mobile/src/api/system/dict/data.js
@@ -0,0 +1,35 @@
+// 字典数据API - 重新导出common/dict.js中的方法以保持兼容性
+import {
+ getDicts,
+ getDict,
+ useDict,
+ getDictLabel,
+ getDictValue,
+ hasDictValue,
+ refreshDict,
+ getDictTagType,
+} from '@/common/dict.js';
+
+// 重新导出所有字典相关方法
+export {
+ getDicts,
+ getDict,
+ useDict,
+ getDictLabel,
+ getDictValue,
+ hasDictValue,
+ refreshDict,
+ getDictTagType,
+};
+
+// 默认导出
+export default {
+ getDicts,
+ getDict,
+ useDict,
+ getDictLabel,
+ getDictValue,
+ hasDictValue,
+ refreshDict,
+ getDictTagType,
+};
diff --git a/mobile/src/common/README-API调用规范.md b/mobile/src/common/README-API调用规范.md
new file mode 100644
index 00000000..c4b02c1b
--- /dev/null
+++ b/mobile/src/common/README-API调用规范.md
@@ -0,0 +1,413 @@
+# 移动端API调用标准规范
+
+> 📌 **重要提示**: 移动端与Web端的API调用方式完全不同,请严格遵守本规范!
+
+## 🚫 常见错误
+
+### ❌ 不要这样做(Web端方式)
+```javascript
+// 错误1:使用 this.$http
+const res = await this.$http.get('/api/path');
+const res = await this.$http.post('/api/path', data);
+const res = await this.$http.upload('/api/upload', options);
+
+// 错误2:使用 getDicts
+import { getDicts } from '@/api/system/dict/data.js';
+const res = await getDicts('dict_type');
+this.dictData = res.data; // Web端返回格式
+
+// 错误3:使用 this.$u.toast
+this.$u.toast('提示信息');
+```
+
+## ✅ 正确做法(移动端方式)
+
+### 1. HTTP请求
+
+```javascript
+// 导入
+import { request } from '@/common/request.js';
+
+// GET请求(无参数)
+const res = await request({
+ url: '/api/path',
+ method: 'GET',
+});
+
+// GET请求(带参数)
+const res = await request({
+ url: '/api/path',
+ method: 'GET',
+ params: {
+ key1: 'value1',
+ key2: 'value2'
+ },
+});
+
+// POST请求
+const res = await request({
+ url: '/api/path',
+ method: 'POST',
+ data: {
+ key1: 'value1',
+ key2: 'value2'
+ },
+});
+
+// PUT请求
+const res = await request({
+ url: '/api/path',
+ method: 'PUT',
+ data: { ... },
+});
+
+// DELETE请求
+const res = await request({
+ url: '/api/path',
+ method: 'DELETE',
+});
+```
+
+### 2. 字典数据获取
+
+```javascript
+// 导入
+import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
+
+// 获取字典数据(直接返回数组)
+const dictData = await getDict('dict_type'); // 返回: [{label, value, ...}, ...]
+
+// 批量获取字典
+const [dict1, dict2] = await Promise.all([
+ getDict('dict_type_1'),
+ getDict('dict_type_2'),
+]);
+
+// 获取字典标签
+const label = getDictLabel(dictData, '1'); // 返回: '标签名'
+
+// 获取字典Tag类型(用于u-tag组件)
+const tagType = getDictTagType(dictData, '1'); // 返回: 'success', 'warning', 'error' 等
+```
+
+### 3. 字典展示(u-tag)
+
+```vue
+
+
+
+ {{ getDictLabel(xg_dict_type, value) }}
+
+
+
+
+ {{ getDictLabel(xg_dict_type, value) }}
+
+
+
+
+
+
+
+```
+
+### 4. 文件上传
+
+```javascript
+// 导入
+import { uploadFile } from '@/utils/upload.js';
+
+// 选择并上传图片
+uni.chooseImage({
+ count: 1,
+ success: async (res) => {
+ const filePath = res.tempFilePaths[0];
+
+ // 上传文件
+ const result = await uploadFile(filePath, (progress) => {
+ console.log('上传进度:', progress);
+ // 更新进度条
+ this.uploadProgress = progress;
+ });
+
+ if (result && result.code === 200 && result.data) {
+ // 上传成功
+ this.ossId = result.data.ossId;
+ this.fileUrl = result.data.url;
+ }
+ }
+});
+```
+
+### 5. Toast提示
+
+```javascript
+// 普通提示
+uni.showToast({
+ title: '提示信息',
+ icon: 'none'
+});
+
+// 成功提示
+uni.showToast({
+ title: '操作成功',
+ icon: 'success'
+});
+
+// 失败提示
+uni.showToast({
+ title: '操作失败',
+ icon: 'error'
+});
+
+// 加载中
+uni.showLoading({
+ title: '加载中...',
+ mask: true
+});
+
+// 关闭加载
+uni.hideLoading();
+```
+
+## 📊 完整示例
+
+```vue
+
+
+
+
+ {{ getDictLabel(xg_status_dict, formData.status) }}
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+
+
+
+
+```
+
+## 🔑 核心差异对照表
+
+| 功能 | Web端 | 移动端 |
+|------|-------|--------|
+| **HTTP请求** | `this.$http.get('/api')` | `request({ url: '/api', method: 'GET' })` |
+| **字典获取** | `getDicts('type')` 返回 `{data:[]}` | `getDict('type')` 返回 `[...]` |
+| **字典标签** | `selectDictLabel(dict, value)` | `getDictLabel(dict, value)` |
+| **Tag类型** | 手动映射 | `getDictTagType(dict, value)` |
+| **文件上传** | `this.$http.upload()` | `uploadFile(path, callback)` |
+| **Toast提示** | `this.$u.toast('msg')` | `uni.showToast({ title: 'msg' })` |
+| **加载提示** | `this.$u.loading.show()` | `uni.showLoading({ title: '...' })` |
+
+## 📝 注意事项
+
+1. **导入路径固定**:
+ - HTTP请求: `@/common/request.js`
+ - 字典方法: `@/common/dict.js`
+ - 文件上传: `@/utils/upload.js`
+
+2. **字典数据格式**:
+ - 移动端 `getDict()` 直接返回数组,不需要 `.data`
+ - 字典项格式: `{ label, value, elTagType, elTagClass, remark }`
+
+3. **Tag类型自动转换**:
+ - `danger` → `error`
+ - `default` → `primary`
+
+4. **文件上传进度**:
+ - 进度回调返回 0-100 的数字
+ - Web端可能是 `progress.progress`,移动端直接是数字
+
+5. **错误处理**:
+ - 统一使用 `try-catch`
+ - 错误时使用 `uni.showToast` 提示
+
+## 🔗 相关文档
+
+- `mobile/src/common/request.js` - HTTP请求源码
+- `mobile/src/common/dict.js` - 字典方法源码
+- `mobile/src/utils/upload.js` - 文件上传源码
+- `.cursor/cursorrules-mobile.md` - 移动端开发规范
+
+---
+
+**最后更新**: 2025-01-24
+**适用范围**: 移动端工程(Vue 2.6.14 + uni-app 2.x)
+
diff --git a/mobile/src/common/config.js b/mobile/src/common/config.js
index 85839def..af11a20f 100644
--- a/mobile/src/common/config.js
+++ b/mobile/src/common/config.js
@@ -12,6 +12,7 @@ export const ENCRYPT_CONFIG = {
TENANT_ID: '000000',
// 加密请求头
ENCRYPT_HEADER: 'encrypt-key',
+ //TODO 开发环境需要修改为false
// 是否启用加密
ENCRYPT_ENABLED: true,
};
@@ -36,8 +37,9 @@ export const REQUEST_CONFIG = {
? 'http://localhost/login?redirect=/index'
: 'https://hzdyjs.camasort.com/login?redirect=/index', // 生产环境前端域名
+ //TODO 生产环境需要修改为true
// 是否启用加密
- encryptEnabled: true,
+ encryptEnabled: false,
// 防重复提交间隔时间(ms)
repeatSubmitInterval: 500,
};
diff --git a/mobile/src/pages.json b/mobile/src/pages.json
index 8db5e1ac..960c5afe 100644
--- a/mobile/src/pages.json
+++ b/mobile/src/pages.json
@@ -200,6 +200,13 @@
"navigationBarTextStyle": "white"
}
},
+ {
+ "path": "pages/xgss/wsjc/wsjcMainAdd",
+ "style": {
+ "navigationBarTitleText": "宿舍卫生检查",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "pages/xgss/xssswsjc/xssswsjc",
"style": {
@@ -303,30 +310,30 @@
"navigationBarTextStyle": "white"
}
},
- {
- "path": "pages/xljk/reservation/index",
- "style": {
- "navigationBarTitleText": "心理咨询预约",
- "navigationBarBackgroundColor": "#0051d9",
- "navigationBarTextStyle": "white"
- }
- },
- {
- "path": "pages/xljk/my-appointments/index",
- "style": {
- "navigationBarTitleText": "我的预约",
- "navigationBarBackgroundColor": "#0051d9",
- "navigationBarTextStyle": "white"
- }
- },
- {
- "path": "pages/xljk/reservation/appointment-form",
- "style": {
- "navigationBarTitleText": "预约咨询",
- "navigationBarBackgroundColor": "#0051d9",
- "navigationBarTextStyle": "white"
- }
- }
+ {
+ "path": "pages/xljk/reservation/index",
+ "style": {
+ "navigationBarTitleText": "心理咨询预约",
+ "navigationBarBackgroundColor": "#0051d9",
+ "navigationBarTextStyle": "white"
+ }
+ },
+ {
+ "path": "pages/xljk/my-appointments/index",
+ "style": {
+ "navigationBarTitleText": "我的预约",
+ "navigationBarBackgroundColor": "#0051d9",
+ "navigationBarTextStyle": "white"
+ }
+ },
+ {
+ "path": "pages/xljk/reservation/appointment-form",
+ "style": {
+ "navigationBarTitleText": "预约咨询",
+ "navigationBarBackgroundColor": "#0051d9",
+ "navigationBarTextStyle": "white"
+ }
+ }
],
"globalStyle": {
"navigationBarTextStyle": "black",
diff --git a/mobile/src/pages/xgss/wsjc/components/README.md b/mobile/src/pages/xgss/wsjc/components/README.md
new file mode 100644
index 00000000..403034bc
--- /dev/null
+++ b/mobile/src/pages/xgss/wsjc/components/README.md
@@ -0,0 +1,210 @@
+# 宿舍卫生检查组件说明
+
+## 组件列表
+
+### 1. SingleRoomCheck.vue - 单宿舍检查组件
+
+**功能说明:**
+- 移动端单个宿舍的卫生检查表单组件
+- 与web端 `DormHygieneCheckSingleDialog.vue` 功能完全一致
+- 用于嵌入在弹窗中进行单个宿舍的详细检查
+
+**主要功能模块:**
+
+#### 1.1 宿舍整体情况
+- 违纪类型选择(从API加载)
+- 宿舍扣分输入(0-100分)
+- 卫生等级选择(字典:`xg_dorm_hygiene_grade`)
+- 检查附件上传(支持图片,最多15个)
+
+#### 1.2 宿舍学生情况
+- 学生基本信息展示(姓名、学号、性别、床位、班级、专业)
+- 学生评分项:
+ - 个人扣分(0-100分)
+ - 出勤状态(字典:`xg_student_attendance_status`)
+ - 学生评级(字典:`xg_dorm_hygiene_grade`)
+- 请假信息展示(如果有请假记录)
+- 违纪记录录入与管理
+
+**Props:**
+```javascript
+{
+ roomInfo: Object, // 房间信息(必填)
+ checkInfo: Object, // 检查基础信息(必填)
+ existingData: Object // 编辑时的现有数据(可选)
+}
+```
+
+**Events:**
+```javascript
+{
+ submit: (data) => {}, // 保存检查结果
+ cancel: () => {} // 取消操作
+}
+```
+
+**API接口:**
+- `GET /disciplineType/disciplineType/listByAreaType` - 加载违纪类型
+- `GET /dormBedStudent/dormBedStudent/getRoomStudents` - 加载房间学生信息
+- `GET /studentLeave/studentLeave/queryByCheckDate` - 查询学生请假信息
+- `POST /dormHygieneCheck/dormHygieneCheck/saveCompleteData` - 保存完整检查数据
+- `POST /resource/oss/upload` - 上传附件
+
+**字典类型:**
+- `xg_dorm_hygiene_grade` - 卫生等级
+- `xg_student_attendance_status` - 出勤状态
+
+---
+
+### 2. ViolationRecordDialog.vue - 违纪记录弹窗
+
+**功能说明:**
+- 学生违纪记录的录入和编辑弹窗
+- 支持新增和编辑两种模式
+
+**主要字段:**
+- 违纪类型(从API加载)
+- 扣分分数(自动填入最低扣分)
+- 违纪日期
+- 违纪地点
+- 违纪描述
+- 处理意见
+
+**Props:**
+```javascript
+{
+ visible: Boolean, // 弹窗显示状态
+ studentInfo: Object, // 学生信息
+ violationData: Object // 违纪数据(编辑模式)
+}
+```
+
+**Events:**
+```javascript
+{
+ confirm: (data) => {}, // 确认保存
+ close: () => {} // 关闭弹窗
+}
+```
+
+**API接口:**
+- `GET /disciplineType/disciplineType/listByAreaType` - 加载违纪类型
+
+---
+
+## 使用示例
+
+### 在 wsjcMainAdd.vue 中使用 SingleRoomCheck
+
+```vue
+
+
+
+
+
+
+
+```
+
+---
+
+## 开发规范
+
+### 1. API调用
+- 统一使用 `mobile/src/common/request.js` 中的 `request` 方法
+- 附件上传使用 `smartUploadFile` 方法(自动适配微信环境)
+
+### 2. 字典数据
+- 使用 `mobile/src/common/dict.js` 中的方法
+ - `getDict(dictType)` - 获取字典数据
+ - `getDictLabel(dictData, value)` - 获取字典标签
+ - `getDictTagType(dictData, value)` - 获取u-tag类型
+
+### 3. 样式规范
+- 使用uview-ui 2.x组件
+- 字典数据用 `` 展示
+- 遵循移动端UI设计规范
+
+### 4. 数据提交
+- 与web端API接口完全一致
+- 参数名和数据结构保持一致
+- 返回数据格式相同
+
+---
+
+## 技术栈
+
+- Vue 2.6.14
+- uni-app 2.x
+- uview-ui 2.0.38
+- dayjs(日期处理)
+
+---
+
+## 注意事项
+
+1. **附件上传兼容性**
+ - 移动端附件功能兼容微信小程序
+ - 参考 `wsjcAdd.vue` 的附件实现
+ - 使用 `smartUploadFile` 方法自动适配环境
+
+2. **字典数据加载**
+ - 组件初始化时自动加载字典数据
+ - 使用缓存机制,避免重复请求
+
+3. **请假信息自动填充**
+ - 加载学生信息时自动查询请假记录
+ - 有请假记录的学生自动设置出勤状态为"请假"
+
+4. **违纪类型自动扣分**
+ - 选择违纪类型后自动填入最低扣分
+ - 用户可手动修改
+
+5. **数据验证**
+ - 扣分范围:0-100分
+ - 必填字段验证
+ - 表单提交前统一验证
+
+---
+
+## 更新日志
+
+**2025-01-24**
+- 创建 SingleRoomCheck.vue 组件
+- 创建 ViolationRecordDialog.vue 组件
+- 实现与web端完全一致的功能
+- 适配移动端UI和交互方式
+
diff --git a/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue b/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue
new file mode 100644
index 00000000..64eb75d9
--- /dev/null
+++ b/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue
@@ -0,0 +1,2212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 图片附件
+
+
+
+
+
+
+
+
+
+
+ {{ image.progress || 0 }}%
+
+
+
+
+
+
+ 上传失败
+
+
+
+
+
+
+
+
+
+
+
+ 下载
+
+
+ 重试
+
+
+ 删除
+
+
+
+
+
+ {{ getShortFileName(image.name) }}
+
+
+
+
+
+
+
+
+
+ 微信图片上传进度: {{ uploadProgress }}%
+
+
+
+
+
+
+ 添加证明材料
+
+
+
+
+
+
+ • 支持格式:JPG、PNG、PDF、Word等 • 单个文件不超过10MB •
+ 最多上传15个文件
+
+ • 图片文件将以图片形式展示,支持预览和下载
+
+ • 在微信中使用可获得更好的图片选择体验
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 班级:{{ student.className }} | 专业:{{ student.majorName }}
+
+
+
+
+
+
+ 个人扣分
+
+
+
+ 出勤状态
+
+
+
+ handleAttendanceConfirm(e, student)"
+ @cancel="student.showAttendancePicker = false"
+ >
+
+
+ 学生评级
+
+
+
+ handleStudentGradeConfirm(e, student)"
+ @cancel="student.showGradePicker = false"
+ >
+
+
+
+
+
+
+
+
+
+ 发起时间:
+
+ {{
+ formatDateTime(
+ getStudentLeave(student.studentId) &&
+ getStudentLeave(student.studentId).createTime,
+ )
+ }}
+
+
+
+ 请假时间:
+
+ {{
+ formatDateTime(
+ getStudentLeave(student.studentId) &&
+ getStudentLeave(student.studentId).startTime,
+ )
+ }}
+ 至
+ {{
+ formatDateTime(
+ getStudentLeave(student.studentId) &&
+ getStudentLeave(student.studentId).endTime,
+ )
+ }}
+
+
+
+ 请假类型:
+
+ {{
+ getStudentLeave(student.studentId) &&
+ getStudentLeave(student.studentId).leaveTypeName
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ violation.disciplineTypeName }}
+
+
+ 扣{{ violation.scoreDeduction }}分
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+ 暂无违纪记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载学生信息中...
+
+
+
+
+
+
+ 取消
+
+
+ 保存检查结果
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue b/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue
new file mode 100644
index 00000000..2a7ac3c6
--- /dev/null
+++ b/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue
@@ -0,0 +1,1901 @@
+
+
+
+
+
+
+
+
+
+
+
+ 学生信息:
+
+
+ {{ studentInfo && studentInfo.studentName }}
+
+
+ ({{ studentInfo && studentInfo.studentNo }})
+
+
+ {{ studentInfo && studentInfo.className }}
+
+
+ {{ studentInfo && studentInfo.bedName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 图片附件
+
+
+
+
+
+
+
+
+
+
+ {{ image.progress || 0 }}%
+
+
+
+
+
+
+ 上传失败
+
+
+
+
+
+
+
+
+
+
+
+ 下载
+
+
+ 重试
+
+
+ 删除
+
+
+
+
+
+ {{ getShortFileName(image.name) }}
+
+
+
+
+
+
+
+
+
+ 微信图片上传进度: {{ uploadProgress }}%
+
+
+
+
+
+
+ 添加证明材料(图片)
+
+
+
+
+
+
+ • 支持格式:JPG、PNG等 • 单个文件不超过10MB •
+ 最多上传5个文件
+
+
+ • 图片文件将以图片形式展示,支持预览和下载
+
+
+ • 在微信中使用可获得更好的图片选择体验
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/pages/xgss/wsjc/wsjc.vue b/mobile/src/pages/xgss/wsjc/wsjc.vue
index fe4531ce..81d5b182 100644
--- a/mobile/src/pages/xgss/wsjc/wsjc.vue
+++ b/mobile/src/pages/xgss/wsjc/wsjc.vue
@@ -450,7 +450,7 @@ export default {
// 新增检查
handleAdd() {
uni.navigateTo({
- url: '/pages/xgss/wsjc/wsjcAdd?type=add',
+ url: '/pages/xgss/wsjc/wsjcMainAdd',
});
},
diff --git a/mobile/src/pages/xgss/wsjc/wsjcMainAdd.vue b/mobile/src/pages/xgss/wsjc/wsjcMainAdd.vue
new file mode 100644
index 00000000..acecd989
--- /dev/null
+++ b/mobile/src/pages/xgss/wsjc/wsjcMainAdd.vue
@@ -0,0 +1,971 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 检查基础信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 点击房间卡片开始检查
+
+
+
+
+
+
+
+
+
+
+ {{ room.roomName }}
+
+ {{ room.roomCode || room.roomName }}
+
+
+
+
+
+
+
+
+ 已检查
+
+ 点击检查
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/src/utils/upload.js b/mobile/src/utils/upload.js
new file mode 100644
index 00000000..c9fa566a
--- /dev/null
+++ b/mobile/src/utils/upload.js
@@ -0,0 +1,308 @@
+// 文件上传工具 - 移动端适配
+import { request } from '@/common/request.js';
+import { REQUEST_CONFIG } from '@/common/config.js';
+
+/**
+ * 获取token
+ */
+function getToken() {
+ try {
+ const token = uni.getStorageSync('access_token');
+ return token ? `Bearer ${token}` : '';
+ } catch (e) {
+ console.error('❌ 获取Token失败:', e);
+ return '';
+ }
+}
+
+/**
+ * 构建完整的上传URL
+ */
+function buildUploadUrl(url) {
+ let fullUrl;
+ if (REQUEST_CONFIG.baseURL.startsWith('/')) {
+ // 开发环境,baseURL是'/api',需要使用完整的服务器URL
+ fullUrl = `${REQUEST_CONFIG.serverUrl}${url}`;
+ } else {
+ // 生产环境,直接拼接
+ fullUrl = REQUEST_CONFIG.baseURL + url;
+ }
+ return fullUrl;
+}
+
+/**
+ * 上传单个文件
+ * @param {string} url - 上传接口URL
+ * @param {Object} options - 上传选项
+ * @param {string} options.filePath - 文件路径
+ * @param {string} options.name - 文件字段名,默认'file'
+ * @param {Object} options.formData - 额外的表单数据
+ * @param {Function} options.onProgress - 进度回调
+ * @returns {Promise}
+ */
+export function uploadFile(url, options = {}) {
+ return new Promise((resolve, reject) => {
+ const { filePath, name = 'file', formData = {}, onProgress } = options;
+
+ if (!filePath) {
+ return reject(new Error('文件路径不能为空'));
+ }
+
+ const token = getToken();
+ const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
+ const fullUrl = buildUploadUrl(url);
+
+ console.log('🚀 开始上传文件:', {
+ url: fullUrl,
+ filePath,
+ name,
+ });
+
+ const uploadTask = uni.uploadFile({
+ url: fullUrl,
+ filePath,
+ name,
+ formData,
+ header: {
+ authorization: token,
+ clientid: clientid,
+ },
+ success: (res) => {
+ console.log('✅ 文件上传成功:', res);
+
+ if (res.statusCode === 200) {
+ try {
+ const data = JSON.parse(res.data);
+ if (data.code === 200) {
+ resolve(data);
+ } else {
+ uni.showToast({
+ title: data.msg || '上传失败',
+ icon: 'none',
+ });
+ reject(new Error(data.msg || '上传失败'));
+ }
+ } catch (error) {
+ console.error('❌ 解析上传响应失败:', error);
+ reject(new Error('解析响应数据失败'));
+ }
+ } else {
+ reject(new Error(`上传失败: HTTP ${res.statusCode}`));
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 文件上传失败:', err);
+ uni.showToast({
+ title: '上传失败',
+ icon: 'none',
+ });
+ reject(new Error(err.errMsg || '上传失败'));
+ },
+ });
+
+ // 监听上传进度
+ if (onProgress && typeof onProgress === 'function') {
+ uploadTask.onProgressUpdate((res) => {
+ onProgress(res);
+ });
+ }
+ });
+}
+
+/**
+ * 上传多个文件
+ * @param {string} url - 上传接口URL
+ * @param {Array} files - 文件列表
+ * @param {Object} options - 上传选项
+ * @returns {Promise}
+ */
+export function uploadFiles(url, files, options = {}) {
+ const uploadPromises = files.map((file, index) => {
+ return uploadFile(url, {
+ ...options,
+ filePath: file.tempFilePath || file.path || file.url,
+ onProgress: (progress) => {
+ if (options.onProgress) {
+ options.onProgress({
+ ...progress,
+ fileIndex: index,
+ totalFiles: files.length,
+ });
+ }
+ },
+ });
+ });
+
+ return Promise.all(uploadPromises);
+}
+
+/**
+ * 选择并上传图片
+ * @param {string} url - 上传接口URL
+ * @param {Object} options - 选择和上传选项
+ * @returns {Promise}
+ */
+export function chooseAndUploadImage(url, options = {}) {
+ return new Promise((resolve, reject) => {
+ const {
+ count = 1,
+ sizeType = ['compressed'],
+ sourceType = ['album', 'camera'],
+ ...uploadOptions
+ } = options;
+
+ uni.chooseImage({
+ count,
+ sizeType,
+ sourceType,
+ success: (chooseRes) => {
+ console.log('📷 选择图片成功:', chooseRes);
+
+ if (count === 1) {
+ // 单张图片上传
+ uploadFile(url, {
+ ...uploadOptions,
+ filePath: chooseRes.tempFilePaths[0],
+ })
+ .then(resolve)
+ .catch(reject);
+ } else {
+ // 多张图片上传
+ uploadFiles(url, chooseRes.tempFiles, uploadOptions)
+ .then(resolve)
+ .catch(reject);
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 选择图片失败:', err);
+ reject(new Error(err.errMsg || '选择图片失败'));
+ },
+ });
+ });
+}
+
+/**
+ * 选择并上传文件
+ * @param {string} url - 上传接口URL
+ * @param {Object} options - 选择和上传选项
+ * @returns {Promise}
+ */
+export function chooseAndUploadFile(url, options = {}) {
+ return new Promise((resolve, reject) => {
+ const { count = 1, type = 'all', extension = [], ...uploadOptions } = options;
+
+ // #ifdef MP-WEIXIN
+ // 微信小程序使用 wx.chooseMessageFile
+ uni.chooseMessageFile({
+ count,
+ type,
+ extension,
+ success: (chooseRes) => {
+ console.log('📁 选择文件成功:', chooseRes);
+
+ if (count === 1) {
+ uploadFile(url, {
+ ...uploadOptions,
+ filePath: chooseRes.tempFiles[0].path,
+ })
+ .then(resolve)
+ .catch(reject);
+ } else {
+ uploadFiles(url, chooseRes.tempFiles, uploadOptions)
+ .then(resolve)
+ .catch(reject);
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 选择文件失败:', err);
+ reject(new Error(err.errMsg || '选择文件失败'));
+ },
+ });
+ // #endif
+
+ // #ifndef MP-WEIXIN
+ // H5和其他平台使用普通文件选择
+ uni.chooseFile({
+ count,
+ type,
+ extension,
+ success: (chooseRes) => {
+ console.log('📁 选择文件成功:', chooseRes);
+
+ if (count === 1) {
+ uploadFile(url, {
+ ...uploadOptions,
+ filePath: chooseRes.tempFilePaths[0],
+ })
+ .then(resolve)
+ .catch(reject);
+ } else {
+ uploadFiles(url, chooseRes.tempFiles, uploadOptions)
+ .then(resolve)
+ .catch(reject);
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 选择文件失败:', err);
+ reject(new Error(err.errMsg || '选择文件失败'));
+ },
+ });
+ // #endif
+ });
+}
+
+/**
+ * 压缩图片
+ * @param {string} src - 图片路径
+ * @param {Object} options - 压缩选项
+ * @returns {Promise}
+ */
+export function compressImage(src, options = {}) {
+ return new Promise((resolve, reject) => {
+ const { quality = 80, compressedWidth, compressedHeight } = options;
+
+ uni.compressImage({
+ src,
+ quality,
+ compressedWidth,
+ compressedHeight,
+ success: (res) => {
+ console.log('✅ 图片压缩成功:', res);
+ resolve(res.tempFilePath);
+ },
+ fail: (err) => {
+ console.error('❌ 图片压缩失败:', err);
+ reject(new Error(err.errMsg || '图片压缩失败'));
+ },
+ });
+ });
+}
+
+/**
+ * 获取图片信息
+ * @param {string} src - 图片路径
+ * @returns {Promise}
+ */
+export function getImageInfo(src) {
+ return new Promise((resolve, reject) => {
+ uni.getImageInfo({
+ src,
+ success: (res) => {
+ resolve(res);
+ },
+ fail: (err) => {
+ reject(new Error(err.errMsg || '获取图片信息失败'));
+ },
+ });
+ });
+}
+
+// 默认导出
+export default {
+ uploadFile,
+ uploadFiles,
+ chooseAndUploadImage,
+ chooseAndUploadFile,
+ compressImage,
+ getImageInfo,
+};