Browse Source

chore: auto bump version and changelog [skip ci]

iwt 2 months ago
parent
commit
8b52f3452a

+ 5 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminFoodController.java

@@ -27,7 +27,11 @@ public class AdminFoodController {
         long size = params.get("size") != null ? ((Number) params.get("size")).longValue() : 20;
         String category = (String) params.get("category");
         String keyword = (String) params.get("keyword");
-        Page<Food> pageResult = foodService.listPage(page, size, category, keyword, "active");
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
+        Page<Food> pageResult = foodService.listPage(page, size, category, keyword, "active", sortSpecs);
         List<Map<String, Object>> records = new ArrayList<>();
         for (Food food : pageResult.getRecords()) {
             Map<String, Object> item = new HashMap<>();

+ 49 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/FoodService.java

@@ -1,6 +1,7 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.Food;
 import com.etotem.cfc.entity.SeasonalFood;
@@ -25,23 +26,65 @@ public class FoodService {
     private SeasonalFoodMapper seasonalFoodMapper;
 
     public List<Food> listAll(String category, String status) {
+        return listAll(category, status, null);
+    }
+
+    public List<Food> listAll(String category, String status, List<Map<String, String>> sortSpecs) {
         LambdaQueryWrapper<Food> wrapper = new LambdaQueryWrapper<Food>()
                 .eq(status != null, Food::getStatus, status)
-                .eq(category != null, Food::getCategory, category)
-                .orderByAsc(Food::getSortOrder);
+                .eq(category != null, Food::getCategory, category);
+        if (sortSpecs != null && !sortSpecs.isEmpty()) {
+            buildOrderBy(wrapper, sortSpecs);
+        } else {
+            wrapper.orderByAsc(Food::getSortOrder);
+        }
         return foodMapper.selectList(wrapper);
     }
 
+    private void buildOrderBy(LambdaQueryWrapper<Food> wrapper, List<Map<String, String>> sortSpecs) {
+        StringBuilder sb = new StringBuilder(" order by ");
+        for (int i = 0; i < sortSpecs.size(); i++) {
+            if (i > 0) sb.append(", ");
+            Map<String, String> spec = sortSpecs.get(i);
+            String col = spec.get("column");
+            if (col == null) continue;
+            boolean asc = !"desc".equalsIgnoreCase(spec.get("order"));
+            sb.append(colToSnake(col)).append(asc ? " asc" : " desc");
+        }
+        wrapper.last(sb.toString());
+    }
+
+    private String colToSnake(String col) {
+        switch (col) {
+            case "name":        return "name";
+            case "category":    return "category";
+            case "calories":    return "kcal_per_100g";
+            case "protein":     return "protein";
+            case "carbs":       return "carbs";
+            case "fat":         return "fat";
+            case "fiber":       return "fiber";
+            case "energyKj":    return "energy_kj";
+            case "starch":      return "starch";
+            case "cholesterol": return "cholesterol";
+            case "sortOrder":   return "sort_order";
+            case "createdAt":   return "created_at";
+            default:            return col.replaceAll("([A-Z])", "_$1").toLowerCase();
+        }
+    }
+
     /**
      * 分页查询食材列表,支持分类、关键字、状态筛选
      */
-    public Page<Food> listPage(long page, long size, String category, String keyword, String status) {
+    public Page<Food> listPage(long page, long size, String category, String keyword, String status, List<Map<String, String>> sortSpecs) {
         LambdaQueryWrapper<Food> wrapper = new LambdaQueryWrapper<Food>()
                 .eq(status != null, Food::getStatus, status)
                 .eq(category != null && !category.isEmpty(), Food::getCategory, category)
-                .like(keyword != null && !keyword.isEmpty(), Food::getName, keyword)
-                .orderByAsc(Food::getSortOrder)
-                .orderByAsc(Food::getId);
+                .like(keyword != null && !keyword.isEmpty(), Food::getName, keyword);
+        if (sortSpecs != null && !sortSpecs.isEmpty()) {
+            buildOrderBy(wrapper, sortSpecs);
+        } else {
+            wrapper.orderByAsc(Food::getSortOrder).orderByAsc(Food::getId);
+        }
         return foodMapper.selectPage(new Page<>(page, size), wrapper);
     }
 

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-160cf39c9ce11775a06c8c51f6efcc214dd50f9a
+23a81259ef1fb5f5b19be00a1c9b8e1fe3e28b2c

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.964",
+  "version": "1.0.965",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.964",
+      "version": "1.0.965",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 56 - 1
cfc-web/src/views/admin/Foods.vue

@@ -290,6 +290,13 @@ export default {
   computed: {
     tableHeight() {
       return window.innerHeight - 340
+    },
+    sortLabel() {
+      if (!this.sortState || this.sortState.length === 0) return ''
+      return this.sortState.map(s => {
+        const col = this.sortColLabels[s.column] || s.column
+        return s.order === 'asc' ? col + '↑' : col + '↓'
+      }).join(' > ')
     }
   },
   created() {
@@ -314,7 +321,55 @@ export default {
       }
     },
     handleSearch() {
-      this.page = 1
+      this.sortState = []
+      this.loadList()
+    },
+    sortClass(col) {
+      const s = this.sortState.find(x => x.column === col)
+      if (!s) return ''
+      return s.order === 'asc' ? 'sort-asc' : 'sort-desc'
+    },
+    sortIcon(col) {
+      const s = this.sortState.find(x => x.column === col)
+      if (!s) return ''
+      return s.order === 'asc' ? ' ↑' : ' ↓'
+    },
+    onSortClick(col, event) {
+      if (event.shiftKey) {
+        this.shiftAppendSort(col)
+      } else {
+        this.setPrimarySort(col)
+      }
+      this.loadList()
+    },
+    onSortToggle(col) {
+      const idx = this.sortState.findIndex(s => s.column === col)
+      if (idx >= 0) {
+        this.sortState[idx].order = this.sortState[idx].order === 'asc' ? 'desc' : 'asc'
+      } else {
+        this.sortState = [{ column: col, order: 'asc' }]
+      }
+      this.loadList()
+    },
+    setPrimarySort(col) {
+      const idx = this.sortState.findIndex(s => s.column === col)
+      if (idx >= 0) {
+        const item = this.sortState.splice(idx, 1)[0]
+        this.sortState.unshift(item)
+      } else {
+        this.sortState = [{ column: col, order: 'asc' }]
+      }
+    },
+    shiftAppendSort(col) {
+      const idx = this.sortState.findIndex(s => s.column === col)
+      if (idx >= 0) {
+        this.sortState[idx].order = this.sortState[idx].order === 'asc' ? 'desc' : 'asc'
+      } else if (this.sortState.length < 3) {
+        this.sortState.push({ column: col, order: 'asc' })
+      }
+    },
+    clearSort() {
+      this.sortState = []
       this.loadList()
     },
     handleSizeChange(val) {