소스 검색

实现引航计划船公司查询条件的远程搜索和分页加载功能

- 将船公司查询条件改为可搜索的下拉框
- 实现远程搜索功能,支持按公司名称搜索
- 实现分页加载功能,每次加载10条记录
- 添加滚动到底部自动加载更多数据的功能
- 更新web-dev-best-practices.md添加搜索下拉框最佳实践
heyiwen 8 시간 전
부모
커밋
9f180ae948
2개의 변경된 파일298개의 추가작업 그리고 11개의 파일을 삭제
  1. 249 9
      .trae/rules/web-dev-best-practices.md
  2. 49 2
      vue-frontend/src/components/PilotPlan.vue

+ 249 - 9
.trae/rules/web-dev-best-practices.md

@@ -272,15 +272,87 @@ const getUsers = async () => {
 ```
 
 **重要说明:**
-- 使用固定宽度(width)而不是最小宽度(min-width)可以避免标题栏分行
-- 序号列使用自定义模板实现连续序号,公式为`(currentPage - 1) * pageSize + $index + 1`,确保分页时序号连续
-- 序号列宽度使用60px,比原来的50px更合适,可以显示更大的页码
-- 操作列使用180px,足够容纳3个图标按钮
-- 状态列使用80px,足够显示状态标签
-- 用户名和账号列使用100px,适合大多数情况
-- 用户角色列使用150px,可以容纳多个角色名称(用逗号分隔)
-- 企业微信账号列使用120px,适合大多数微信号长度
-- 日期时间列使用160px,可以完整显示日期时间格式
+  - 使用固定宽度(width)而不是最小宽度(min-width)可以避免标题栏分行
+  - 序号列使用自定义模板实现连续序号,公式为`(currentPage - 1) * pageSize + $index + 1`,确保分页时序号连续
+  - 序号列宽度使用60px,比原来的50px更合适,可以显示更大的页码
+  - 操作列使用180px,足够容纳3个图标按钮
+  - 状态列使用80px,足够显示状态标签
+  - 用户名和账号列使用100px,适合大多数情况
+  - 用户角色列使用150px,可以容纳多个角色名称(用逗号分隔)
+  - 企业微信账号列使用120px,适合大多数微信号长度
+  - 日期时间列使用160px,可以完整显示日期时间格式
+
+### 2.4 图标按钮使用规范
+
+操作列应使用图标按钮,而不是文字按钮,使界面更加简洁美观。
+
+#### 2.4.1 图标按钮示例
+
+```vue
+<el-table-column label="操作" width="180" align="center" fixed="right">
+  <template #default="{ row }">
+    <el-tooltip content="编辑" placement="top">
+      <el-button type="primary" :icon="Edit" circle size="small" @click="edit(row)"></el-button>
+    </el-tooltip>
+    <el-tooltip content="删除" placement="top">
+      <el-button type="danger" :icon="Delete" circle size="small" @click="delete(row.id)"></el-button>
+    </el-tooltip>
+  </template>
+</el-table-column>
+```
+
+#### 2.4.2 图标导入和使用
+
+```javascript
+import { Edit, Delete, User, Lock, Search } from '@element-plus/icons-vue'
+
+export default {
+  components: {
+    Edit,
+    Delete,
+    User,
+    Lock,
+    Search
+  }
+}
+```
+
+#### 2.4.3 常用图标说明
+
+| 图标 | 名称 | 用途 |
+|------|------|------|
+| `Edit` | 编辑 | 修改数据 |
+| `Delete` | 删除 | 删除数据 |
+| `User` | 用户 | 用户/人员管理 |
+| `Lock` | 锁定 | 锁定/重置密码 |
+| `Search` | 搜索 | 搜索/查询 |
+| `Plus` | 加号 | 新增 |
+| `View` | 查看 | 查看详情 |
+| `Download` | 下载 | 下载文件 |
+| `Upload` | 上传 | 上传文件 |
+| `Refresh` | 刷新 | 刷新数据 |
+
+#### 2.4.4 图标按钮属性说明
+
+- `type`:按钮类型,如`primary`、`success`、`warning`、`danger`、`info`
+- `:icon`:图标组件,如`:icon="Edit"`
+- `circle`:圆形按钮
+- `size`:按钮尺寸,推荐使用`small`
+- `@click`:点击事件
+
+#### 2.4.5 Tooltip使用
+
+使用`el-tooltip`为图标按钮提供功能提示:
+
+```vue
+<el-tooltip content="编辑" placement="top">
+  <el-button type="primary" :icon="Edit" circle size="small" @click="edit(row)"></el-button>
+</el-tooltip>
+```
+
+**Tooltip属性:**
+- `content`:提示文本
+- `placement`:提示位置,可选值:`top`、`bottom`、`left`、`right`
 
 ### 2.3 日期时间格式化
 
@@ -592,3 +664,171 @@ request.interceptors.request.use(config => {
   点击
 </el-button>
 ```
+
+## 11. 搜索下拉框规范
+
+### 11.1 适用场景
+
+对于选项数量较多的下拉选择框(如船公司、代理公司、港口等),应使用可搜索的下拉框,并实现远程搜索和分页加载功能。
+
+### 11.2 搜索下拉框实现
+
+#### 11.2.1 模板部分
+
+```vue
+<el-form-item label="船公司">
+  <el-select
+    v-model="searchForm.shipownerId"
+    placeholder="请选择"
+    style="width: 180px"
+    clearable
+    filterable
+    remote
+    :remote-method="searchShipCompany"
+    :loading="shipCompanyLoading"
+    @visible-change="handleShipCompanyVisibleChange"
+    @scroll="handleShipCompanyScroll"
+  >
+    <el-option
+      v-for="company in shipCompanyList"
+      :key="company.companyId"
+      :label="company.companyName"
+      :value="company.companyId">
+    </el-option>
+  </el-select>
+</el-form-item>
+```
+
+#### 11.2.2 数据属性
+
+```javascript
+data() {
+  return {
+    searchForm: {
+      shipownerId: ''
+    },
+    shipCompanyList: [],
+    shipCompanyQuery: '',
+    shipCompanyPage: 1,
+    shipCompanyPageSize: 10,
+    shipCompanyLoading: false,
+    shipCompanyHasMore: true
+  }
+}
+```
+
+#### 11.2.3 方法实现
+
+```javascript
+methods: {
+  getShipCompanyList() {
+    this.getShipCompanyListByPage('', 1);
+  },
+  getShipCompanyListByPage(query, page) {
+    this.shipCompanyLoading = true;
+    this.$axios.get('/api/ship-company/list', {
+      params: {
+        companyName: query,
+        page: page,
+        pageSize: this.shipCompanyPageSize
+      }
+    })
+      .then(response => {
+        if (page === 1) {
+          this.shipCompanyList = response.data.content || [];
+        } else {
+          this.shipCompanyList = [...this.shipCompanyList, ...(response.data.content || [])];
+        }
+        this.shipCompanyHasMore = response.data.content && response.data.content.length >= this.shipCompanyPageSize;
+      })
+      .catch(error => {
+        console.error('获取船公司列表失败:', error);
+        this.$message.error('获取船公司列表失败,请稍后重试');
+      })
+      .finally(() => {
+        this.shipCompanyLoading = false;
+      });
+  },
+  searchShipCompany(query) {
+    this.shipCompanyQuery = query || '';
+    this.shipCompanyPage = 1;
+    this.getShipCompanyListByPage(this.shipCompanyQuery, this.shipCompanyPage);
+  },
+  handleShipCompanyVisibleChange(visible) {
+    if (visible && !this.shipCompanyQuery) {
+      this.shipCompanyPage = 1;
+      this.getShipCompanyListByPage('', this.shipCompanyPage);
+    }
+  },
+  handleShipCompanyScroll() {
+    if (this.shipCompanyHasMore && !this.shipCompanyLoading) {
+      this.shipCompanyPage++;
+      this.getShipCompanyListByPage(this.shipCompanyQuery, this.shipCompanyPage);
+    }
+  }
+}
+```
+
+### 11.3 后端API规范
+
+#### 11.3.1 Controller实现
+
+```java
+@GetMapping("/list")
+public Page<ShipCompanyDTO> list(
+    @RequestParam(required = false) String companyName,
+    @RequestParam(defaultValue = "1") int page,
+    @RequestParam(defaultValue = "10") int pageSize
+) {
+    Pageable pageable = PageRequest.of(page - 1, pageSize, Sort.by("companyName").ascending());
+    return shipCompanyService.searchCompanies(companyName, pageable);
+}
+```
+
+#### 11.3.2 Service实现
+
+```java
+@Service
+public class ShipCompanyService {
+    
+    @Autowired
+    private ShipCompanyRepository shipCompanyRepository;
+    
+    public Page<ShipCompanyDTO> searchCompanies(String companyName, Pageable pageable) {
+        Specification<ShipCompany> spec = (root, query, cb) -> {
+            List<Predicate> predicates = new ArrayList<>();
+            
+            if (companyName != null && !companyName.isEmpty()) {
+                predicates.add(cb.like(root.get("companyName"), "%" + companyName + "%"));
+            }
+            
+            predicates.add(cb.equal(root.get("recordStatus"), 1));
+            
+            return cb.and(predicates.toArray(new Predicate[0]));
+        };
+        
+        Page<ShipCompany> companies = shipCompanyRepository.findAll(spec, pageable);
+        return companies.map(ShipCompanyDTO::fromEntity);
+    }
+}
+```
+
+### 11.4 关键属性说明
+
+| 属性 | 说明 |
+|------|------|
+| filterable | 启用本地过滤功能 |
+| remote | 启用远程搜索 |
+| remote-method | 远程搜索方法,当用户输入时触发 |
+| loading | 加载状态,用于显示加载动画 |
+| visible-change | 下拉框显示/隐藏时触发 |
+| scroll | 下拉列表滚动时触发,用于实现瀑布加载 |
+
+### 11.5 注意事项
+
+1. **分页大小**:建议每页加载10-20条记录
+2. **防抖处理**:远程搜索建议添加防抖,避免频繁请求
+3. **错误处理**:必须添加错误处理和用户提示
+4. **加载状态**:必须显示加载状态,提升用户体验
+5. **清空处理**:清空搜索条件时,应重置分页和列表
+6. **首次加载**:下拉框首次打开时,应加载第一页数据

+ 49 - 2
vue-frontend/src/components/PilotPlan.vue

@@ -83,6 +83,12 @@
               placeholder="请选择"
               style="width: 180px"
               clearable
+              filterable
+              remote
+              :remote-method="searchShipCompany"
+              :loading="shipCompanyLoading"
+              @visible-change="handleShipCompanyVisibleChange"
+              @scroll="handleShipCompanyScroll"
             >
               <el-option
                 v-for="company in shipCompanyList"
@@ -296,6 +302,11 @@ export default {
       pilotPlans: [],
       portList: [],
       shipCompanyList: [],
+      shipCompanyQuery: '',
+      shipCompanyPage: 1,
+      shipCompanyPageSize: 10,
+      shipCompanyLoading: false,
+      shipCompanyHasMore: true,
       currentPage: 1,
       pageSize: 20,
       total: 0,
@@ -399,14 +410,50 @@ export default {
         });
     },
     getShipCompanyList() {
-      this.$axios.get('/api/ship-company/list')
+      this.getShipCompanyListByPage('', 1);
+    },
+    getShipCompanyListByPage(query, page) {
+      this.shipCompanyLoading = true;
+      this.$axios.get('/api/ship-company/list', {
+        params: {
+          companyName: query,
+          page: page,
+          pageSize: this.shipCompanyPageSize
+        }
+      })
         .then(response => {
-          this.shipCompanyList = response.data;
+          if (page === 1) {
+            this.shipCompanyList = response.data.content || [];
+          } else {
+            this.shipCompanyList = [...this.shipCompanyList, ...(response.data.content || [])];
+          }
+          this.shipCompanyHasMore = response.data.content && response.data.content.length >= this.shipCompanyPageSize;
         })
         .catch(error => {
           console.error('获取船公司列表失败:', error);
+          this.$message.error('获取船公司列表失败,请稍后重试');
+        })
+        .finally(() => {
+          this.shipCompanyLoading = false;
         });
     },
+    searchShipCompany(query) {
+      this.shipCompanyQuery = query || '';
+      this.shipCompanyPage = 1;
+      this.getShipCompanyListByPage(this.shipCompanyQuery, this.shipCompanyPage);
+    },
+    handleShipCompanyVisibleChange(visible) {
+      if (visible && !this.shipCompanyQuery) {
+        this.shipCompanyPage = 1;
+        this.getShipCompanyListByPage('', this.shipCompanyPage);
+      }
+    },
+    handleShipCompanyScroll() {
+      if (this.shipCompanyHasMore && !this.shipCompanyLoading) {
+        this.shipCompanyPage++;
+        this.getShipCompanyListByPage(this.shipCompanyQuery, this.shipCompanyPage);
+      }
+    },
     getPilotPlans() {
       if (this.loading) return;