Browse Source

fix: 修复API接口规范和数据获取问题

1. 修复前端从res.rows改为res.data获取数据
2. 更新API接口基础规范文档:
   - 添加系统基础模块规范
   - 添加租户过滤规范
   - 统一返回值规范
   - 完善代码生成规范
zlwu 1 year ago
parent
commit
a4247ea901
2 changed files with 382 additions and 33 deletions
  1. 193 0
      docs/开发文档/API/API文档基础规范.md
  2. 189 33
      web/src/views/hq/hallReservation/index.vue

+ 193 - 0
docs/开发文档/API/API文档基础规范.md

@@ -346,3 +346,196 @@
 -   次要按钮: 使用轮廓式设计
 -   危险按钮: 使用错误色
 -   禁用状态: 降低透明度
+
+# API接口基础规范(2025-07-01更新)
+
+## 一、系统基础模块规范
+
+### 1. 系统模块代码位置
+
+所有涉及以下功能的API接口代码必须统一写在 `ruoyi-system` 工程中:
+- 用户管理相关(SysUser)
+- 组织机构相关(SysDept)
+- 角色权限相关(SysRole)
+- 菜单权限相关(SysMenu)
+- 字典管理相关(SysDict)
+- 系统配置相关(SysConfig)
+- 租户管理相关(SysTenant)
+
+#### 代码组织结构
+```
+ruoyi-system/
+├── src/main/java/org/dromara/system/
+│   ├── controller/system/      # 控制器(注意是system子包)
+│   ├── service/               # 服务接口
+│   ├── service/impl/         # 服务实现
+│   ├── mapper/              # 数据访问层
+│   └── domain/             # 实体类和VO对象
+```
+
+### 2. 租户过滤规范
+
+#### 2.1 Controller层
+```java
+@RestController
+@RequestMapping("/system/xxx")
+public class XxxController {
+    
+    @GetMapping("/list")
+    public R<List<XxxVO>> list(@RequestParam(value = "tenantId", required = false) String tenantId) {
+        // 如果tenantId为空,会自动使用当前用户的租户ID
+        return R.ok(xxxService.selectXxxList(tenantId));
+    }
+}
+```
+
+#### 2.2 Service层
+```java
+@Service
+public class XxxServiceImpl implements IXxxService {
+    
+    @Override
+    public List<XxxVO> selectXxxList(String tenantId) {
+        if (StringUtils.isEmpty(tenantId)) {
+            tenantId = SecurityUtils.getTenantId();
+        }
+        return baseMapper.selectXxxList(tenantId);
+    }
+}
+```
+
+#### 2.3 Mapper层
+```java
+@Mapper
+public interface XxxMapper {
+    
+    @Select("""
+        SELECT 
+            x.*
+        FROM xxx x
+        WHERE x.tenant_id = #{tenantId}
+            AND x.del_flag = '0'
+        ORDER BY x.create_time DESC
+    """)
+    List<Xxx> selectXxxList(@Param("tenantId") String tenantId);
+}
+```
+
+### 3. 统一返回值规范
+
+#### 3.1 后端返回格式
+```java
+// 统一返回格式
+public class R<T> {
+    private int code;           // 状态码
+    private String msg;         // 消息
+    private T data;            // 数据对象
+}
+
+// 分页返回格式
+public class TableDataInfo<T> {
+    private int code;           // 状态码
+    private String msg;         // 消息
+    private List<T> data;      // 数据对象
+    private long total;         // 总记录数
+}
+```
+
+#### 3.2 前端获取数据规范
+```typescript
+// 标准数据获取
+const getList = async () => {
+  try {
+    const res = await listApi();
+    // 正确:从data字段获取数据
+    dataList.value = res.data || [];
+  } catch (error) {
+    console.error('获取数据失败:', error);
+    dataList.value = [];
+  }
+};
+
+// 分页数据获取
+const getPageList = async (query: PageQuery) => {
+  try {
+    const res = await pageApi(query);
+    // 正确:从data字段获取列表数据
+    dataList.value = res.data || [];
+    // 获取总数
+    total.value = res.total || 0;
+  } catch (error) {
+    console.error('获取分页数据失败:', error);
+    dataList.value = [];
+    total.value = 0;
+  }
+};
+```
+
+## 二、代码生成规范
+
+### 1. 代码生成器配置
+
+#### 1.1 系统模块配置
+- 当生成的代码涉及用户、组织、角色等基础功能时:
+  - 包名:`org.dromara.system`
+  - 模块名:`ruoyi-system`
+  - 表前缀:`sys_`
+
+#### 1.2 租户字段配置
+- 必须包含租户字段:`tenant_id`
+- 字段类型:`varchar(20)`
+- 默认值:`'000000'`
+- 注释:`租户编号`
+
+### 2. 模板调整建议
+
+#### 2.1 Controller模板
+```java
+@ApiOperation("查询${functionName}列表")
+@SaCheckPermission("${permissionPrefix}:list")
+@GetMapping("/list")
+public R<List<${ClassName}VO>> list(${ClassName}BO bo) {
+    // 确保设置租户ID
+    if (StringUtils.isEmpty(bo.getTenantId())) {
+        bo.setTenantId(SecurityUtils.getTenantId());
+    }
+    return R.ok(${className}Service.queryList(bo));
+}
+```
+
+#### 2.2 Service模板
+```java
+@Override
+public List<${ClassName}VO> queryList(${ClassName}BO bo) {
+    // 租户过滤
+    LambdaQueryWrapper<${ClassName}> lqw = Wrappers.lambdaQuery();
+    lqw.eq(${ClassName}::getTenantId, bo.getTenantId());
+    // ... 其他查询条件
+    return baseMapper.selectVoList(lqw);
+}
+```
+
+## 三、注意事项
+
+### 1. 租户ID处理
+- 前端可以选择性传入租户ID
+- 如果未传入,后端使用当前用户的租户ID
+- 禁止跨租户查询(除非特殊说明)
+
+### 2. 返回值处理
+- 前端统一从data字段获取数据
+- 禁止使用rows或其他字段名
+- 空值处理使用空数组或空对象
+
+### 3. 系统模块开发
+- 涉及基础功能必须在ruoyi-system中开发
+- 保持代码结构统一
+- 遵循权限命名规范
+
+---
+
+**📝 维护说明**:
+- 更新时间:2025-07-01
+- 更新人:wuzilong
+- 基于实际项目开发经验总结
+- 持续补充和优化

+ 189 - 33
web/src/views/hq/hallReservation/index.vue

@@ -37,23 +37,26 @@
             <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['hq:hallReservation:add']">新增</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button  icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:hallReservation:edit']"
-              >修改</el-button
-            >
+            <el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['hq:hallReservation:edit']">修改</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button  icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:hallReservation:remove']"
-              >删除</el-button
-            >
+            <el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['hq:hallReservation:remove']">删除</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button  icon="Download" @click="handleExport" v-hasPermi="['hq:hallReservation:export']">导出</el-button>
+            <el-button icon="Download" @click="handleExport" v-hasPermi="['hq:hallReservation:export']">导出</el-button>
           </el-col>
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
 
-      <el-table v-loading="loading" border :data="hallReservationList" @selection-change="handleSelectionChange" style="margin-bottom: 0">
+      <el-table
+        v-loading="loading"
+        border
+        :data="hallReservationList"
+        @selection-change="handleSelectionChange"
+        style="margin-bottom: 0"
+        @row-dblclick="handleView"
+      >
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="报告厅ID" align="center" prop="hallId" />
         <el-table-column label="报告厅名称" align="center" prop="hallName" />
@@ -78,13 +81,9 @@
               :hide-after="100"
               effect="dark"
             >
-              <span class="purpose-text">
-                {{
-                  scope.row.reservationPurpose && scope.row.reservationPurpose.length > 100
-                    ? scope.row.reservationPurpose.substring(0, 100) + '...'
-                    : scope.row.reservationPurpose
-                }}
-              </span>
+              <div class="purpose-text">
+                {{ scope.row.reservationPurpose }}
+              </div>
             </el-tooltip>
           </template>
         </el-table-column>
@@ -110,17 +109,13 @@
               :hide-after="100"
               effect="dark"
             >
-              <span class="audit-remark-text">
-                {{
-                  scope.row.auditRemark && scope.row.auditRemark.length > 100
-                    ? scope.row.auditRemark.substring(0, 100) + '...'
-                    : scope.row.auditRemark
-                }}
-              </span>
+              <div class="audit-remark-text">
+                {{ scope.row.auditRemark }}
+              </div>
             </el-tooltip>
           </template>
         </el-table-column>
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right">
           <template #default="scope">
             <!-- 查看按钮,所有记录都可以查看 -->
             <el-tooltip content="查看" placement="top">
@@ -206,9 +201,9 @@
           </el-select>
         </el-form-item>
         <!-- 报告厅图片展示区域 -->
-        <el-form-item label="报告厅图片" v-if="selectedHallImages.length > 0">
+        <el-form-item label="报告厅图片">
           <div class="selected-hall-images">
-            <div class="image-gallery">
+            <div class="image-gallery" v-if="selectedHallImages.length > 0">
               <div v-for="(image, index) in selectedHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
                 <img :src="image" class="gallery-image" @error="handleImageError" />
                 <div class="image-overlay">
@@ -216,6 +211,10 @@
                 </div>
               </div>
             </div>
+            <div v-else class="no-images-placeholder">
+              <el-icon><Picture /></el-icon>
+              <span>暂无图片</span>
+            </div>
           </div>
         </el-form-item>
         <el-form-item label="预约开始时间" prop="startTime">
@@ -226,6 +225,7 @@
             value-format="YYYY-MM-DD HH:mm:ss"
             placeholder="请选择预约开始时间"
             style="width: 100%"
+            @change="handleStartTimeChange"
           >
           </el-date-picker>
         </el-form-item>
@@ -269,9 +269,9 @@
           <span class="form-text">{{ viewForm.hallName }}</span>
         </el-form-item>
         <!-- 报告厅图片展示区域 -->
-        <el-form-item label="报告厅图片" v-if="viewHallImages.length > 0">
+        <el-form-item label="报告厅图片">
           <div class="view-hall-images">
-            <div class="image-gallery">
+            <div class="image-gallery" v-if="viewHallImages.length > 0">
               <div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
                 <img :src="image" class="gallery-image" @error="handleImageError" />
                 <div class="image-overlay">
@@ -279,6 +279,10 @@
                 </div>
               </div>
             </div>
+            <div v-else class="no-images-placeholder">
+              <el-icon><Picture /></el-icon>
+              <span>暂无图片</span>
+            </div>
           </div>
         </el-form-item>
         <el-form-item label="预约开始时间">
@@ -301,6 +305,19 @@
         <el-form-item label="预约状态">
           <dict-tag :options="hall_reserv_status" :value="viewForm.status" />
         </el-form-item>
+        <!-- 当状态为审核中时显示可审核人员列表 -->
+        <el-form-item label="可审核人员" v-if="viewForm.status === '0' && auditorList.length > 0">
+          <div class="auditor-list">
+            <div v-for="auditor in auditorList" :key="auditor.userId" class="auditor-item">
+              <el-avatar :size="32" :src="auditor.avatar">{{ auditor.nickName?.substring(0, 1) }}</el-avatar>
+              <div class="auditor-info">
+                <div class="auditor-name">{{ auditor.nickName || auditor.userName }}</div>
+                <div class="auditor-dept">{{ auditor.dept?.deptName }}</div>
+              </div>
+            </div>
+            <div v-if="auditorList.length === 0" class="no-auditor">暂无可审核人员</div>
+          </div>
+        </el-form-item>
         <el-form-item label="审核人" v-if="viewForm.auditName">
           <span class="form-text">{{ viewForm.auditName }}</span>
         </el-form-item>
@@ -325,7 +342,7 @@
 <script setup name="HallReservation" lang="ts">
 import { listHallReservation, getHallReservation, delHallReservation, addHallReservation, updateHallReservation } from '@/api/hq/hallReservation';
 import { listHallInfo } from '@/api/hq/hallInfo';
-import { getUserProfile } from '@/api/system/user';
+import { getUserProfile, listUsersByPermission } from '@/api/system/user';
 import { HallReservationVO, HallReservationQuery, HallReservationForm } from '@/api/hq/hallReservation/types';
 import { HallInfoVO } from '@/api/hq/hallInfo/types';
 import { useUserStore } from '@/store/modules/user';
@@ -375,6 +392,44 @@ const viewForm = ref<HallReservationForm>({
   auditRemark: undefined
 });
 
+// 添加审核人员列表
+const auditorList = ref<any[]>([]);
+
+/** 获取具有审核权限的人员列表 */
+const getAuditorList = async () => {
+  try {
+    const res = await listUsersByPermission('hq:hallAudit:list');
+    auditorList.value = res.data || [];
+    console.log('审核人员列表:', auditorList.value);
+  } catch (error) {
+    console.error('获取审核人员列表失败:', error);
+    auditorList.value = [];
+  }
+};
+
+/** 验证结束时间 */
+const validateEndTime = (rule: any, value: any, callback: any) => {
+  if (value && form.value.startTime) {
+    const startTime = new Date(form.value.startTime).getTime();
+    const endTime = new Date(value).getTime();
+    if (endTime <= startTime) {
+      callback(new Error('结束时间必须晚于开始时间'));
+    } else {
+      callback();
+    }
+  } else {
+    callback();
+  }
+};
+
+/** 开始时间变化时触发结束时间验证 */
+const handleStartTimeChange = () => {
+  // 如果结束时间已经填写,则触发验证
+  if (data.form.endTime) {
+    hallReservationFormRef.value?.validateField('endTime');
+  }
+};
+
 const initFormData: HallReservationForm = {
   id: undefined,
   hallId: undefined,
@@ -390,6 +445,7 @@ const initFormData: HallReservationForm = {
   auditTime: undefined,
   auditRemark: undefined
 };
+
 const data = reactive<PageData<HallReservationForm, HallReservationQuery>>({
   form: { ...initFormData },
   queryParams: {
@@ -409,7 +465,25 @@ const data = reactive<PageData<HallReservationForm, HallReservationQuery>>({
   rules: {
     hallName: [{ required: true, message: '报告厅名称不能为空', trigger: 'change' }],
     startTime: [{ required: true, message: '预约开始时间不能为空', trigger: 'blur' }],
-    endTime: [{ required: true, message: '预约结束时间不能为空', trigger: 'blur' }],
+    endTime: [
+      { required: true, message: '预约结束时间不能为空', trigger: 'blur' },
+      {
+        validator: (rule: any, value: any, callback: any) => {
+          if (value && data.form.startTime) {
+            const startTime = new Date(data.form.startTime).getTime();
+            const endTime = new Date(value).getTime();
+            if (endTime <= startTime) {
+              callback(new Error('结束时间必须晚于开始时间'));
+            } else {
+              callback();
+            }
+          } else {
+            callback();
+          }
+        },
+        trigger: 'blur'
+      }
+    ],
     reservationPurpose: [{ required: true, message: '预约用途不能为空', trigger: 'blur' }]
   }
 });
@@ -635,6 +709,11 @@ const handleView = async (row: HallReservationVO) => {
   const res = await getHallReservation(row.id);
   Object.assign(viewForm.value, res.data);
 
+  // 如果状态是审核中,获取审核人员列表
+  if (viewForm.value.status === '0') {
+    await getAuditorList();
+  }
+
   // 查找对应的报告厅并显示图片
   if (viewForm.value.hallName) {
     const selectedHall = hallList.value.find((hall) => hall.hallName === viewForm.value.hallName);
@@ -849,22 +928,28 @@ onMounted(() => {
 
 /* 表格中预约用途文本样式 */
 .purpose-text {
-  display: inline-block;
-  max-width: 100%;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 3;
+  overflow: hidden;
   word-break: break-all;
   word-wrap: break-word;
   line-height: 1.4;
   cursor: pointer;
+  max-height: 4.2em;
 }
 
 /* 表格中审核备注文本样式 */
 .audit-remark-text {
-  display: inline-block;
-  max-width: 100%;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 3;
+  overflow: hidden;
   word-break: break-all;
   word-wrap: break-word;
   line-height: 1.4;
   cursor: pointer;
+  max-height: 4.2em;
 }
 
 /* 查看详情表单样式 */
@@ -923,6 +1008,7 @@ onMounted(() => {
   display: flex;
   flex-wrap: wrap;
   gap: 12px;
+  max-width: 450px; /* 与预约用途保持一致 */
 }
 
 .image-item {
@@ -970,6 +1056,76 @@ onMounted(() => {
   color: white;
   font-size: 20px;
 }
+
+/* 暂无图片占位样式 */
+.no-images-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 450px; /* 与预约用途保持一致 */
+  height: 120px;
+  background-color: #f5f7fa;
+  border: 2px dashed #dcdfe6;
+  border-radius: 8px;
+  color: #909399;
+}
+
+.no-images-placeholder .el-icon {
+  font-size: 32px;
+  margin-bottom: 8px;
+}
+
+.no-images-placeholder span {
+  font-size: 14px;
+}
+
+/* 审核人员列表样式 */
+.auditor-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  padding: 8px 0;
+}
+
+.auditor-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 16px;
+  background-color: #f5f7fa;
+  border-radius: 8px;
+  transition: all 0.3s ease;
+}
+
+.auditor-item:hover {
+  background-color: #ecf5ff;
+  transform: translateY(-2px);
+}
+
+.auditor-info {
+  display: flex;
+  flex-direction: column;
+}
+
+.auditor-name {
+  font-size: 14px;
+  color: #303133;
+  font-weight: 500;
+}
+
+.auditor-dept {
+  font-size: 12px;
+  color: #909399;
+}
+
+.no-auditor {
+  color: #909399;
+  font-size: 14px;
+  font-style: italic;
+}
+
+/* 页面容器样式 */
 </style>
 
 <style>