提交代码
This commit is contained in:
1 parent
aacdbdad81
commit
a0e5c9a024
11 files changed
+6086
-26
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
<template>
|
||||
<!-- 基础用法 -->
|
||||
<u-tag :type="getDictTagType(xg_dict_type, value)">
|
||||
{{ getDictLabel(xg_dict_type, value) }}
|
||||
</u-tag>
|
||||
|
||||
<!-- 带降级处理 -->
|
||||
<u-tag :type="getDictTagType(xg_dict_type, value, tagTypeFallback)">
|
||||
{{ getDictLabel(xg_dict_type, value) }}
|
||||
</u-tag>
|
||||
|
||||
<!-- 选择器显示 -->
|
||||
<u-picker
|
||||
:show="showPicker"
|
||||
:columns="dictColumns"
|
||||
keyName="text"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="showPicker = false"
|
||||
></u-picker>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
xg_dict_type: [],
|
||||
value: '1',
|
||||
showPicker: false,
|
||||
|
||||
// 降级映射(当字典未加载时使用)
|
||||
tagTypeFallback: {
|
||||
'1': 'success',
|
||||
'2': 'warning',
|
||||
'3': 'error',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 转换为选择器格式
|
||||
dictColumns() {
|
||||
return [
|
||||
this.xg_dict_type.map(item => ({
|
||||
text: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
];
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
// 加载字典
|
||||
this.xg_dict_type = await getDict('xg_dict_type');
|
||||
},
|
||||
methods: {
|
||||
getDictLabel,
|
||||
getDictTagType,
|
||||
|
||||
handleConfirm(e) {
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
this.value = e.value[0].value;
|
||||
}
|
||||
this.showPicker = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 字典展示 -->
|
||||
<u-tag :type="getDictTagType(xg_status_dict, formData.status)">
|
||||
{{ getDictLabel(xg_status_dict, formData.status) }}
|
||||
</u-tag>
|
||||
|
||||
<!-- 字典选择器 -->
|
||||
<u-form-item label="状态" prop="status">
|
||||
<view @click="showStatusPicker = true">
|
||||
<u-input
|
||||
:value="getDictLabel(xg_status_dict, formData.status)"
|
||||
placeholder="请选择状态"
|
||||
readonly
|
||||
suffix-icon="arrow-down"
|
||||
/>
|
||||
</view>
|
||||
<u-picker
|
||||
:show="showStatusPicker"
|
||||
:columns="statusColumns"
|
||||
keyName="text"
|
||||
@confirm="handleStatusConfirm"
|
||||
@cancel="showStatusPicker = false"
|
||||
></u-picker>
|
||||
</u-form-item>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<u-button type="primary" @click="handleSubmit">提交</u-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { request } from '@/common/request.js';
|
||||
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
|
||||
import { uploadFile } from '@/utils/upload.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
title: '',
|
||||
status: '1',
|
||||
},
|
||||
xg_status_dict: [],
|
||||
showStatusPicker: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statusColumns() {
|
||||
return [
|
||||
this.xg_status_dict.map(item => ({
|
||||
text: item.label,
|
||||
value: item.value,
|
||||
}))
|
||||
];
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.init();
|
||||
},
|
||||
methods: {
|
||||
getDictLabel,
|
||||
getDictTagType,
|
||||
|
||||
async init() {
|
||||
// 加载字典
|
||||
this.xg_status_dict = await getDict('xg_status');
|
||||
|
||||
// 加载数据
|
||||
await this.loadData();
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/api/getData',
|
||||
method: 'GET',
|
||||
params: { id: this.id },
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
this.formData = res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleStatusConfirm(e) {
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
this.formData.status = e.value[0].value;
|
||||
}
|
||||
this.showStatusPicker = false;
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
try {
|
||||
uni.showLoading({ title: '提交中...', mask: true });
|
||||
|
||||
const res = await request({
|
||||
url: '/api/submit',
|
||||
method: 'POST',
|
||||
data: this.formData,
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (res.code === 200) {
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.msg || '提交失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error('提交失败:', error);
|
||||
uni.showToast({
|
||||
title: '提交失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
## 🔑 核心差异对照表
|
||||
|
||||
| 功能 | 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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+31
-24
@@ -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",
|
||||
|
||||
@@ -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
|
||||
<template>
|
||||
<u-popup
|
||||
:show="showRoomCheckPopup"
|
||||
mode="bottom"
|
||||
:round="20"
|
||||
closeable
|
||||
:safe-area-inset-bottom="true"
|
||||
@close="handlePopupClose"
|
||||
>
|
||||
<view class="popup-container">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">
|
||||
{{ currentRoom ? currentRoom.roomName : '' }} - 卫生检查
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
|
||||
<SingleRoomCheck
|
||||
v-if="showRoomCheckPopup && currentRoom"
|
||||
:room-info="currentRoom"
|
||||
:check-info="formData"
|
||||
:existing-data="currentEditData"
|
||||
@submit="handleSingleCheckSubmit"
|
||||
@cancel="handlePopupClose"
|
||||
/>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SingleRoomCheck from './components/SingleRoomCheck.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SingleRoomCheck,
|
||||
},
|
||||
// ...
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 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组件
|
||||
- 字典数据用 `<u-tag>` 展示
|
||||
- 遵循移动端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和交互方式
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -450,7 +450,7 @@ export default {
|
||||
// 新增检查
|
||||
handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/xgss/wsjc/wsjcAdd?type=add',
|
||||
url: '/pages/xgss/wsjc/wsjcMainAdd',
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,971 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 顶部导航 -->
|
||||
<u-navbar
|
||||
title="宿舍卫生检查"
|
||||
:border-bottom="false"
|
||||
bg-color="#0051d9"
|
||||
:titleStyle="{ color: '#fff' }"
|
||||
>
|
||||
<view slot="left" @click="goBack">
|
||||
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
|
||||
</view>
|
||||
</u-navbar>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<u-loading-page :loading="pageLoading" loading-text="加载中..."></u-loading-page>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<view v-if="!pageLoading" class="content-container">
|
||||
<!-- 基础信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-title">检查基础信息</view>
|
||||
<u-form ref="formRef" :model="formData" :rules="rules" label-width="100">
|
||||
<!-- 检查标题 -->
|
||||
<u-form-item label="检查标题" prop="checkTitle" required>
|
||||
<u-input v-model="formData.checkTitle" placeholder="请输入检查标题" />
|
||||
</u-form-item>
|
||||
|
||||
<!-- 检查日期 -->
|
||||
<u-form-item label="检查日期" prop="checkDate" required>
|
||||
<view @click="showDatePicker = true" style="width: 100%">
|
||||
<u-input
|
||||
v-model="checkDateDisplay"
|
||||
placeholder="请选择检查日期"
|
||||
readonly
|
||||
border="none"
|
||||
suffix-icon="arrow-down"
|
||||
suffix-icon-style="color: #c8c9cc;"
|
||||
style="pointer-events: none"
|
||||
/>
|
||||
</view>
|
||||
<u-datetime-picker
|
||||
:show="showDatePicker"
|
||||
v-model="checkDateValue"
|
||||
mode="datetime"
|
||||
@confirm="handleDateConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
></u-datetime-picker>
|
||||
</u-form-item>
|
||||
|
||||
<!-- 检查员 -->
|
||||
<u-form-item label="检查员" prop="checkerName" required>
|
||||
<u-input v-model="formData.checkerName" placeholder="检查员姓名" readonly />
|
||||
</u-form-item>
|
||||
|
||||
<!-- 检查类型 -->
|
||||
<u-form-item label="检查类型" prop="checkType" required>
|
||||
<view @click="showCheckTypePicker = true" style="width: 100%">
|
||||
<u-input
|
||||
v-model="checkTypeDisplay"
|
||||
placeholder="请选择检查类型"
|
||||
readonly
|
||||
border="none"
|
||||
suffix-icon="arrow-down"
|
||||
suffix-icon-style="color: #c8c9cc;"
|
||||
style="pointer-events: none"
|
||||
/>
|
||||
</view>
|
||||
<u-picker
|
||||
:show="showCheckTypePicker && xg_dorm_hygiene_check_type.length > 0"
|
||||
:columns="checkTypeColumns"
|
||||
:defaultIndex="checkTypeDefaultIndex"
|
||||
keyName="text"
|
||||
@confirm="handleCheckTypeConfirm"
|
||||
@cancel="showCheckTypePicker = false"
|
||||
></u-picker>
|
||||
</u-form-item>
|
||||
|
||||
<!-- 检查级别 -->
|
||||
<u-form-item label="检查级别" prop="checkLevel" required>
|
||||
<view @click="showCheckLevelPicker = true" style="width: 100%">
|
||||
<u-input
|
||||
v-model="checkLevelDisplay"
|
||||
placeholder="请选择检查级别"
|
||||
readonly
|
||||
border="none"
|
||||
suffix-icon="arrow-down"
|
||||
suffix-icon-style="color: #c8c9cc;"
|
||||
style="pointer-events: none"
|
||||
/>
|
||||
</view>
|
||||
<u-picker
|
||||
:show="showCheckLevelPicker && xg_dorm_hygiene_check_level.length > 0"
|
||||
:columns="checkLevelColumns"
|
||||
:defaultIndex="checkLevelDefaultIndex"
|
||||
keyName="text"
|
||||
@confirm="handleCheckLevelConfirm"
|
||||
@cancel="showCheckLevelPicker = false"
|
||||
></u-picker>
|
||||
</u-form-item>
|
||||
|
||||
<!-- 校区 -->
|
||||
<u-form-item label="校区" prop="campusId" required>
|
||||
<view @click="showCampusPicker = true" style="width: 100%">
|
||||
<u-input
|
||||
v-model="formData.campusName"
|
||||
placeholder="请选择校区"
|
||||
readonly
|
||||
border="none"
|
||||
suffix-icon="arrow-down"
|
||||
suffix-icon-style="color: #c8c9cc;"
|
||||
style="pointer-events: none"
|
||||
/>
|
||||
</view>
|
||||
<u-picker
|
||||
:show="showCampusPicker"
|
||||
:columns="campusColumns"
|
||||
keyName="text"
|
||||
@confirm="handleCampusConfirm"
|
||||
@cancel="showCampusPicker = false"
|
||||
></u-picker>
|
||||
</u-form-item>
|
||||
|
||||
<!-- 宿舍楼 -->
|
||||
<u-form-item label="宿舍楼" prop="buildingId" required>
|
||||
<view @click="handleBuildingClick" style="width: 100%">
|
||||
<u-input
|
||||
v-model="formData.buildingName"
|
||||
placeholder="请选择宿舍楼"
|
||||
readonly
|
||||
border="none"
|
||||
suffix-icon="arrow-down"
|
||||
suffix-icon-style="color: #c8c9cc;"
|
||||
:disabled="!formData.campusId"
|
||||
style="pointer-events: none"
|
||||
/>
|
||||
</view>
|
||||
<u-picker
|
||||
:show="showBuildingPicker"
|
||||
:columns="buildingColumns"
|
||||
keyName="text"
|
||||
@confirm="handleBuildingConfirm"
|
||||
@cancel="showBuildingPicker = false"
|
||||
></u-picker>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
</view>
|
||||
|
||||
<!-- 房间列表区域 -->
|
||||
<view v-if="formData.buildingId && floorRooms.length > 0" class="rooms-section">
|
||||
<view class="rooms-header">
|
||||
<text class="rooms-title">宿舍房间列表</text>
|
||||
<view class="rooms-stats">
|
||||
<text>总房间: {{ totalRoomsCount }}</text>
|
||||
<text style="margin-left: 20rpx">已检查: {{ completedRoomsCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="hint-text">
|
||||
<u-icon name="info-circle" color="#ff9900" size="16"></u-icon>
|
||||
<text style="margin-left: 10rpx; color: #ff9900">点击房间卡片开始检查</text>
|
||||
</view>
|
||||
|
||||
<!-- 楼层和房间展示 -->
|
||||
<view class="floors-container">
|
||||
<view
|
||||
v-for="floorData in floorRooms"
|
||||
:key="floorData.floor"
|
||||
class="floor-section"
|
||||
>
|
||||
<view class="floor-header">
|
||||
<view class="floor-name">{{ floorData.floor }}楼</view>
|
||||
<view class="floor-stats">
|
||||
已检查: {{ getFloorCompletedCount(floorData.floor) }} /
|
||||
{{ floorData.rooms.length }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rooms-grid">
|
||||
<view
|
||||
v-for="room in floorData.rooms"
|
||||
:key="room.id"
|
||||
class="room-card"
|
||||
:class="{ completed: completedRooms.has(room.id) }"
|
||||
@click="handleRoomClick(room)"
|
||||
>
|
||||
<view class="room-card-content">
|
||||
<view class="room-name">{{ room.roomName }}</view>
|
||||
<view class="room-code">
|
||||
{{ room.roomCode || room.roomName }}
|
||||
</view>
|
||||
<view class="room-status">
|
||||
<u-icon
|
||||
v-if="completedRooms.has(room.id)"
|
||||
name="checkmark-circle-fill"
|
||||
color="#19be6b"
|
||||
size="18"
|
||||
></u-icon>
|
||||
<u-icon
|
||||
v-else
|
||||
name="calendar"
|
||||
color="#c8c9cc"
|
||||
size="18"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="room-hint">
|
||||
<text v-if="completedRooms.has(room.id)" class="completed-text">
|
||||
已检查
|
||||
</text>
|
||||
<text v-else class="pending-text">点击检查</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view
|
||||
v-if="formData.buildingId && floorRooms.length === 0 && !roomsLoading"
|
||||
class="empty-container"
|
||||
>
|
||||
<u-empty text="该宿舍楼暂无房间信息" mode="data"></u-empty>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 单个房间检查弹窗 -->
|
||||
<u-popup
|
||||
:show="showRoomCheckPopup"
|
||||
mode="bottom"
|
||||
:round="20"
|
||||
closeable
|
||||
:safe-area-inset-bottom="true"
|
||||
@close="handlePopupClose"
|
||||
>
|
||||
<view class="popup-container">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">
|
||||
{{ currentRoom ? currentRoom.roomName : '' }} - 卫生检查
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
|
||||
<SingleRoomCheck
|
||||
v-if="showRoomCheckPopup && currentRoom"
|
||||
:room-info="currentRoom"
|
||||
:check-info="formData"
|
||||
:existing-data="currentEditData"
|
||||
@submit="handleSingleCheckSubmit"
|
||||
@cancel="handlePopupClose"
|
||||
/>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
|
||||
import { request } from '@/common/request.js';
|
||||
import dayjs from 'dayjs';
|
||||
import SingleRoomCheck from './components/SingleRoomCheck.vue';
|
||||
|
||||
export default {
|
||||
name: 'WsjcMainAdd',
|
||||
components: {
|
||||
SingleRoomCheck,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageLoading: true,
|
||||
roomsLoading: false,
|
||||
|
||||
// 表单数据
|
||||
formData: {
|
||||
checkTitle: '',
|
||||
checkDate: '',
|
||||
checkerId: '',
|
||||
checkerName: '',
|
||||
checkType: '1',
|
||||
checkLevel: '1',
|
||||
campusId: '',
|
||||
campusName: '',
|
||||
buildingId: '',
|
||||
buildingName: '',
|
||||
},
|
||||
|
||||
// 验证规则
|
||||
rules: {
|
||||
checkTitle: [{ required: true, message: '请输入检查标题', trigger: 'blur' }],
|
||||
checkDate: [{ required: true, message: '请选择检查日期', trigger: 'change' }],
|
||||
checkerName: [{ required: true, message: '请选择检查员', trigger: 'change' }],
|
||||
checkType: [{ required: true, message: '请选择检查类型', trigger: 'change' }],
|
||||
checkLevel: [{ required: true, message: '请选择检查级别', trigger: 'change' }],
|
||||
campusId: [{ required: true, message: '请选择校区', trigger: 'change' }],
|
||||
buildingId: [{ required: true, message: '请选择宿舍楼', trigger: 'change' }],
|
||||
},
|
||||
|
||||
// Picker显示状态
|
||||
showDatePicker: false,
|
||||
showCheckTypePicker: false,
|
||||
showCheckLevelPicker: false,
|
||||
showCampusPicker: false,
|
||||
showBuildingPicker: false,
|
||||
showRoomCheckPopup: false,
|
||||
|
||||
// 日期时间
|
||||
checkDateValue: Date.now(),
|
||||
checkDateDisplay: '',
|
||||
|
||||
// 字典数据
|
||||
xg_dorm_hygiene_check_type: [],
|
||||
xg_dorm_hygiene_check_level: [],
|
||||
|
||||
// 选项数据
|
||||
campusOptions: [],
|
||||
buildingOptions: [],
|
||||
|
||||
// 房间相关数据
|
||||
floorRooms: [],
|
||||
allRooms: [],
|
||||
completedRooms: new Set(),
|
||||
roomCheckIds: new Map(),
|
||||
|
||||
// 当前操作的房间
|
||||
currentRoom: null,
|
||||
currentEditData: null,
|
||||
|
||||
// 弹窗高度
|
||||
popupHeight: 600,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['userInfo']),
|
||||
|
||||
// 检查类型列
|
||||
checkTypeColumns() {
|
||||
return [
|
||||
this.xg_dorm_hygiene_check_type.map((item) => ({
|
||||
text: item.label,
|
||||
value: item.value,
|
||||
})),
|
||||
];
|
||||
},
|
||||
|
||||
// 检查类型默认索引
|
||||
checkTypeDefaultIndex() {
|
||||
const index = this.xg_dorm_hygiene_check_type.findIndex(
|
||||
(item) => item.value === this.formData.checkType,
|
||||
);
|
||||
return [index >= 0 ? index : 0];
|
||||
},
|
||||
|
||||
// 检查类型显示文本
|
||||
checkTypeDisplay() {
|
||||
const item = this.xg_dorm_hygiene_check_type.find(
|
||||
(d) => d.value === this.formData.checkType,
|
||||
);
|
||||
return item ? item.label : '';
|
||||
},
|
||||
|
||||
// 检查级别列
|
||||
checkLevelColumns() {
|
||||
return [
|
||||
this.xg_dorm_hygiene_check_level.map((item) => ({
|
||||
text: item.label,
|
||||
value: item.value,
|
||||
})),
|
||||
];
|
||||
},
|
||||
|
||||
// 检查级别默认索引
|
||||
checkLevelDefaultIndex() {
|
||||
const index = this.xg_dorm_hygiene_check_level.findIndex(
|
||||
(item) => item.value === this.formData.checkLevel,
|
||||
);
|
||||
return [index >= 0 ? index : 0];
|
||||
},
|
||||
|
||||
// 检查级别显示文本
|
||||
checkLevelDisplay() {
|
||||
const item = this.xg_dorm_hygiene_check_level.find(
|
||||
(d) => d.value === this.formData.checkLevel,
|
||||
);
|
||||
return item ? item.label : '';
|
||||
},
|
||||
|
||||
// 校区列
|
||||
campusColumns() {
|
||||
return [this.campusOptions.map((item) => ({ text: item.campusName, value: item.id }))];
|
||||
},
|
||||
|
||||
// 宿舍楼列
|
||||
buildingColumns() {
|
||||
return [
|
||||
this.buildingOptions.map((item) => ({
|
||||
text: this.getBuildingDisplayName(item),
|
||||
value: item.id,
|
||||
})),
|
||||
];
|
||||
},
|
||||
|
||||
// 总房间数
|
||||
totalRoomsCount() {
|
||||
return this.allRooms.length;
|
||||
},
|
||||
|
||||
// 已完成房间数
|
||||
completedRoomsCount() {
|
||||
return this.completedRooms.size;
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
this.initPage();
|
||||
},
|
||||
onReady() {
|
||||
// 计算弹窗高度(屏幕高度的70%)
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.popupHeight = systemInfo.windowHeight * 0.7;
|
||||
},
|
||||
methods: {
|
||||
// 初始化页面
|
||||
async initPage() {
|
||||
try {
|
||||
this.pageLoading = true;
|
||||
|
||||
// 初始化检查标题和日期
|
||||
this.generateDefaultCheckTitle();
|
||||
this.formData.checkDate = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
this.checkDateDisplay = this.formData.checkDate;
|
||||
this.checkDateValue = Date.now();
|
||||
|
||||
// 获取当前用户信息
|
||||
await this.fetchCurrentUserInfo();
|
||||
|
||||
// 加载字典数据
|
||||
await this.loadDicts();
|
||||
|
||||
// 加载校区选项
|
||||
await this.loadCampusOptions();
|
||||
} catch (error) {
|
||||
console.error('初始化页面失败:', error);
|
||||
this.$u.toast('初始化页面失败');
|
||||
} finally {
|
||||
this.pageLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 生成默认检查标题
|
||||
generateDefaultCheckTitle() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
this.formData.checkTitle = `${year}${month}${day}宿舍卫生检查`;
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
async fetchCurrentUserInfo() {
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/system/user/getInfo',
|
||||
method: 'GET',
|
||||
});
|
||||
if (res.code === 200 && res.data?.user) {
|
||||
const user = res.data.user;
|
||||
this.formData.checkerId = user.userId;
|
||||
this.formData.checkerName = user.nickName || user.userName;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取用户信息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载字典数据
|
||||
async loadDicts() {
|
||||
try {
|
||||
const [checkTypeData, checkLevelData] = await Promise.all([
|
||||
getDict('xg_dorm_hygiene_check_type'),
|
||||
getDict('xg_dorm_hygiene_check_level'),
|
||||
]);
|
||||
|
||||
this.xg_dorm_hygiene_check_type = checkTypeData || [];
|
||||
this.xg_dorm_hygiene_check_level = checkLevelData || [];
|
||||
} catch (error) {
|
||||
console.error('加载字典数据失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载校区选项
|
||||
async loadCampusOptions() {
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/xqxx/dormCampus/list',
|
||||
method: 'GET',
|
||||
});
|
||||
this.campusOptions = res.rows || [];
|
||||
} catch (error) {
|
||||
console.error('加载校区选项失败:', error);
|
||||
uni.showToast({ title: '加载校区选项失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 加载宿舍楼选项
|
||||
async loadBuildingOptions(campusId) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/sslxx/dormBuilding/list',
|
||||
method: 'GET',
|
||||
params: { campusId },
|
||||
});
|
||||
this.buildingOptions = res.rows || [];
|
||||
} catch (error) {
|
||||
console.error('加载宿舍楼选项失败:', error);
|
||||
uni.showToast({ title: '加载宿舍楼选项失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 加载房间列表
|
||||
async loadRoomsByFloor(buildingId) {
|
||||
try {
|
||||
this.roomsLoading = true;
|
||||
const res = await request({
|
||||
url: '/xgss/dormRoom/list',
|
||||
method: 'GET',
|
||||
params: { buildingId },
|
||||
});
|
||||
|
||||
if (res.code === 200 && res.rows) {
|
||||
this.allRooms = res.rows;
|
||||
|
||||
// 按楼层分组
|
||||
const floorMap = new Map();
|
||||
res.rows.forEach((room) => {
|
||||
const floor = room.floorNum || '1';
|
||||
if (!floorMap.has(floor)) {
|
||||
floorMap.set(floor, []);
|
||||
}
|
||||
floorMap.get(floor).push(room);
|
||||
});
|
||||
|
||||
// 转换为数组并排序
|
||||
this.floorRooms = Array.from(floorMap.entries())
|
||||
.map(([floor, rooms]) => ({ floor, rooms }))
|
||||
.sort((a, b) => parseInt(a.floor) - parseInt(b.floor));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载房间失败:', error);
|
||||
uni.showToast({ title: '加载房间失败', icon: 'none' });
|
||||
} finally {
|
||||
this.roomsLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 日期确认处理
|
||||
handleDateConfirm(e) {
|
||||
this.formData.checkDate = dayjs(e.value).format('YYYY-MM-DD HH:mm:ss');
|
||||
this.checkDateDisplay = this.formData.checkDate;
|
||||
this.showDatePicker = false;
|
||||
},
|
||||
|
||||
// 检查类型确认处理
|
||||
handleCheckTypeConfirm(e) {
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
this.formData.checkType = e.value[0].value;
|
||||
}
|
||||
this.showCheckTypePicker = false;
|
||||
},
|
||||
|
||||
// 检查级别确认处理
|
||||
handleCheckLevelConfirm(e) {
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
this.formData.checkLevel = e.value[0].value;
|
||||
}
|
||||
this.showCheckLevelPicker = false;
|
||||
},
|
||||
|
||||
// 校区确认处理
|
||||
async handleCampusConfirm(e) {
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
const campus = this.campusOptions.find((item) => item.id === e.value[0].value);
|
||||
if (campus) {
|
||||
this.formData.campusId = campus.id;
|
||||
this.formData.campusName = campus.campusName;
|
||||
|
||||
// 清空下级选项
|
||||
this.formData.buildingId = '';
|
||||
this.formData.buildingName = '';
|
||||
this.buildingOptions = [];
|
||||
this.floorRooms = [];
|
||||
this.allRooms = [];
|
||||
this.completedRooms.clear();
|
||||
this.roomCheckIds.clear();
|
||||
|
||||
await this.loadBuildingOptions(campus.id);
|
||||
}
|
||||
}
|
||||
this.showCampusPicker = false;
|
||||
},
|
||||
|
||||
// 宿舍楼点击处理
|
||||
handleBuildingClick() {
|
||||
if (!this.formData.campusId) {
|
||||
this.$u.toast('请先选择校区');
|
||||
return;
|
||||
}
|
||||
this.showBuildingPicker = true;
|
||||
},
|
||||
|
||||
// 宿舍楼确认处理
|
||||
async handleBuildingConfirm(e) {
|
||||
if (e && e.value && e.value.length > 0) {
|
||||
const building = this.buildingOptions.find((item) => item.id === e.value[0].value);
|
||||
if (building) {
|
||||
this.formData.buildingId = building.id;
|
||||
this.formData.buildingName = building.buildingName;
|
||||
|
||||
// 清空房间数据
|
||||
this.floorRooms = [];
|
||||
this.allRooms = [];
|
||||
this.completedRooms.clear();
|
||||
this.roomCheckIds.clear();
|
||||
|
||||
await this.loadRoomsByFloor(building.id);
|
||||
}
|
||||
}
|
||||
this.showBuildingPicker = false;
|
||||
},
|
||||
|
||||
// 房间点击处理
|
||||
async handleRoomClick(room) {
|
||||
console.log('🔍 handleRoomClick - 点击的房间数据:', room);
|
||||
console.log(
|
||||
'🔍 房间字段 - roomName:',
|
||||
room.roomName,
|
||||
', roomCode:',
|
||||
room.roomCode,
|
||||
', name:',
|
||||
room.name,
|
||||
);
|
||||
|
||||
// 验证基础信息是否完整
|
||||
if (
|
||||
!this.formData.checkTitle ||
|
||||
!this.formData.checkDate ||
|
||||
!this.formData.checkerName ||
|
||||
!this.formData.checkType ||
|
||||
!this.formData.checkLevel
|
||||
) {
|
||||
uni.showToast({ title: '请先完善检查基础信息', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentRoom = room;
|
||||
console.log('✅ currentRoom已赋值:', this.currentRoom);
|
||||
|
||||
// 检查是否已有记录ID
|
||||
const checkId = this.roomCheckIds.get(room.id);
|
||||
|
||||
if (checkId) {
|
||||
// 已检查过,加载数据用于编辑
|
||||
try {
|
||||
const response = await request({
|
||||
url: `/dormHygieneCheck/dormHygieneCheck/${checkId}`,
|
||||
method: 'GET',
|
||||
});
|
||||
if (response.code === 200 && response.data) {
|
||||
this.currentEditData = response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询检查记录详情失败:', error);
|
||||
uni.showToast({ title: '查询检查记录失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.currentEditData = null;
|
||||
}
|
||||
|
||||
this.showRoomCheckPopup = true;
|
||||
},
|
||||
|
||||
// 单个房间检查提交处理
|
||||
handleSingleCheckSubmit(data) {
|
||||
// 标记房间为已完成
|
||||
this.completedRooms.add(this.currentRoom.id);
|
||||
|
||||
// 保存检查记录ID
|
||||
if (data.id) {
|
||||
this.roomCheckIds.set(this.currentRoom.id, data.id);
|
||||
}
|
||||
|
||||
uni.showToast({ title: `${this.currentRoom.roomName} 检查完成`, icon: 'success' });
|
||||
this.showRoomCheckPopup = false;
|
||||
this.currentRoom = null;
|
||||
this.currentEditData = null;
|
||||
},
|
||||
|
||||
// 弹窗关闭处理
|
||||
handlePopupClose() {
|
||||
this.showRoomCheckPopup = false;
|
||||
this.currentRoom = null;
|
||||
this.currentEditData = null;
|
||||
},
|
||||
|
||||
// 获取楼层已完成数量
|
||||
getFloorCompletedCount(floor) {
|
||||
const floorData = this.floorRooms.find((f) => f.floor === floor);
|
||||
if (!floorData) return 0;
|
||||
return floorData.rooms.filter((room) => this.completedRooms.has(room.id)).length;
|
||||
},
|
||||
|
||||
// 宿舍楼显示名称
|
||||
getBuildingDisplayName(building) {
|
||||
let displayName = building.buildingName;
|
||||
if (building.buildingType) {
|
||||
if (building.buildingType === '1') {
|
||||
displayName = `(男)${displayName}`;
|
||||
} else if (building.buildingType === '2') {
|
||||
displayName = `(女)${displayName}`;
|
||||
}
|
||||
}
|
||||
return displayName;
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
console.log('🔙 goBack - 点击返回按钮');
|
||||
|
||||
// 检查是否有未保存的数据
|
||||
if (this.completedRoomsCount > 0) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `已检查 ${this.completedRoomsCount} 个房间,确定要返回吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: (err) => {
|
||||
console.error('❌ navigateBack失败:', err);
|
||||
// 如果navigateBack失败,尝试使用redirectTo
|
||||
uni.redirectTo({
|
||||
url: '/pages/xgss/wsjc/wsjc',
|
||||
fail: (err2) => {
|
||||
console.error('❌ redirectTo也失败:', err2);
|
||||
uni.showToast({ title: '返回失败', icon: 'none' });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: (err) => {
|
||||
console.error('❌ navigateBack失败:', err);
|
||||
// 如果navigateBack失败,尝试使用redirectTo
|
||||
uni.redirectTo({
|
||||
url: '/pages/xgss/wsjc/wsjc',
|
||||
fail: (err2) => {
|
||||
console.error('❌ redirectTo也失败:', err2);
|
||||
uni.showToast({ title: '返回失败', icon: 'none' });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f6f7;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 20rpx;
|
||||
padding-top: 100rpx; /* 为导航栏预留空间 */
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 20rpx;
|
||||
padding-left: 20rpx;
|
||||
border-left: 6rpx solid #0051d9;
|
||||
}
|
||||
|
||||
.rooms-section {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.rooms-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.rooms-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.rooms-stats {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx;
|
||||
background-color: #fff8e6;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.floors-container {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.floor-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.floor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 2rpx solid #3c9cff;
|
||||
margin-bottom: 20rpx;
|
||||
min-height: 60rpx;
|
||||
}
|
||||
|
||||
.floor-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #3c9cff;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.floor-stats {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
min-width: 240rpx;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.rooms-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.room-card {
|
||||
background-color: #fff;
|
||||
border: 2rpx solid #e4e7ed;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.room-card.completed {
|
||||
border-color: #19be6b;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.room-card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
min-height: 80rpx;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
text-align: center;
|
||||
padding-right: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 50rpx;
|
||||
}
|
||||
|
||||
.room-code {
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.room-status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 50rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-hint {
|
||||
text-align: center;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.completed-text {
|
||||
color: #19be6b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pending-text {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
padding: 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.popup-scroll {
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -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<Array>}
|
||||
*/
|
||||
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,
|
||||
};
|
||||
Reference in new issue
Block a user