瀏覽代碼

feat(admin): 食材列表支持多列排序

- 后端: FoodService.listPage() 接受 sort 参数,动态构建 ORDER BY
- 后端: AdminFoodController 解析前端 sort 数组传入 Service
- 前端: 列头点击排序(单击设主排序,Shift+Click追加,Ctrl+Click切换方向)
- 前端: 排序徽章显示当前排序链,最多支持3列组合排序
- 前端: 排序列高亮 + 方向图标
iwt 1 月之前
父節點
當前提交
4b4f60b1e1
共有 1 個文件被更改,包括 127 次插入16 次删除
  1. 127 16
      cfc-web/src/views/admin/Foods.vue

+ 127 - 16
cfc-web/src/views/admin/Foods.vue

@@ -28,6 +28,16 @@
             <el-option label="全部分类" value="" />
             <el-option v-for="opt in categoryOptions" :key="opt" :label="opt" :value="opt" />
           </el-select>
+          <el-tag
+                v-if="sortState.length > 0"
+                closable
+                size="mini"
+                class="sort-tag"
+                @close="clearSort"
+                @click="clearSort"
+              >
+                {{ sortLabel }}
+              </el-tag>
           <el-button type="primary" size="mini" icon="el-icon-plus" @click="openCreate">新建食材</el-button>
         </div>
       </div>
@@ -42,65 +52,95 @@
         :max-height="tableHeight"
       >
         <el-table-column type="index" label="#" width="50" align="center" />
-        <el-table-column prop="name" label="名称" min-width="120" fixed>
+        <el-table-column prop="name" label="名称" min-width="120" fixed :class-name="sortClass('name')">
           <template slot-scope="{ row }">
             <span style="font-weight: 500;">{{ row.name }}</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('name',$event)" @dblclick.stop="onSortToggle('name')">名称{{ sortIcon('name') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column prop="category" label="分类" width="120" align="center">
+        <el-table-column prop="category" label="分类" width="120" align="center" :class-name="sortClass('category')">
           <template slot-scope="{ row }">
             <el-tag :type="categoryTagType(row.category)" size="mini" effect="plain">
               {{ row.category }}
             </el-tag>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('category',$event)" @dblclick.stop="onSortToggle('category')">分类{{ sortIcon('category') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="热量" width="100" align="right">
+        <el-table-column label="热量" width="100" align="right" :class-name="sortClass('calories')">
           <template slot-scope="{ row }">
             <span class="num-val">{{ row.calories != null ? row.calories : '-' }}</span>
             <span class="num-unit">kcal</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('calories',$event)" @dblclick.stop="onSortToggle('calories')">热量{{ sortIcon('calories') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="蛋白质" width="90" align="right">
+        <el-table-column label="蛋白质" width="90" align="right" :class-name="sortClass('protein')">
           <template slot-scope="{ row }">
             <span class="num-val">{{ row.protein != null ? row.protein : '-' }}</span>
             <span class="num-unit">g</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('protein',$event)" @dblclick.stop="onSortToggle('protein')">蛋白{{ sortIcon('protein') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="脂肪" width="80" align="right">
+        <el-table-column label="脂肪" width="80" align="right" :class-name="sortClass('fat')">
           <template slot-scope="{ row }">
             <span class="num-val">{{ row.fat != null ? row.fat : '-' }}</span>
             <span class="num-unit">g</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('fat',$event)" @dblclick.stop="onSortToggle('fat')">脂肪{{ sortIcon('fat') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="碳水" width="80" align="right">
+        <el-table-column label="碳水" width="80" align="right" :class-name="sortClass('carbs')">
           <template slot-scope="{ row }">
             <span class="num-val">{{ row.carbs != null ? row.carbs : '-' }}</span>
             <span class="num-unit">g</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('carbs',$event)" @dblclick.stop="onSortToggle('carbs')">碳水{{ sortIcon('carbs') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="膳食纤维" width="90" align="right">
+        <el-table-column label="膳食纤维" width="90" align="right" :class-name="sortClass('fiber')">
           <template slot-scope="{ row }">
             <span class="num-val">{{ row.fiber != null ? row.fiber : '-' }}</span>
             <span class="num-unit">g</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('fiber',$event)" @dblclick.stop="onSortToggle('fiber')">纤维{{ sortIcon('fiber') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="能量(kJ)" width="90" align="right">
+        <el-table-column label="能量(kJ)" width="90" align="right" :class-name="sortClass('energyKj')">
           <template slot-scope="{ row }">
             <span class="num-val">{{ row.energyKj != null ? row.energyKj : '-' }}</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('energyKj',$event)" @dblclick.stop="onSortToggle('energyKj')">能量{{ sortIcon('energyKj') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column label="胆固醇" width="90" align="right">
+        <el-table-column label="胆固醇" width="90" align="right" :class-name="sortClass('cholesterol')">
           <template slot-scope="{ row }">
             <span v-if="row.cholesterol != null" class="num-val">{{ row.cholesterol }}<span class="num-unit">mg</span></span>
             <span v-else>-</span>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('cholesterol',$event)" @dblclick.stop="onSortToggle('cholesterol')">胆固醇{{ sortIcon('cholesterol') }}</span>
+          </template>
         </el-table-column>
-        <el-table-column prop="status" label="状态" width="80" align="center">
+        <el-table-column prop="status" label="状态" width="80" align="center" :class-name="sortClass('status')">
           <template slot-scope="{ row }">
             <el-tag :type="row.status === 'active' ? 'success' : 'info'" size="mini" effect="dark">
               {{ row.status === 'active' ? '启用' : '停用' }}
             </el-tag>
           </template>
+          <template slot="header">
+            <span class="sort-header" @click.stop="onSortClick('status',$event)" @dblclick.stop="onSortToggle('status')">状态{{ sortIcon('status') }}</span>
+          </template>
         </el-table-column>
         <el-table-column label="操作" width="180" fixed="right" align="center">
           <template slot-scope="{ row }">
@@ -270,6 +310,12 @@ export default {
         '蔬菜', '水果', '水产品', '肉类', '干果',
         '主食', '豆类及豆制品', '蛋类', '乳制品', '快餐', '汤'
       ],
+      sortState: [],
+      sortColLabels: {
+        name: '\u540d\u79f0', category: '\u5206\u7c7b', calories: '\u70ed\u91cf', protein: '\u86cb\u767d',
+        carbs: '\u78b3\u6c34', fat: '\u8102\u80aa', fiber: '\u7ea4\u7ef4', energyKj: '\u80fd\u91cf',
+        starch: '\u6dc0\u7c89', cholesterol: '\u80c6\u56fa\u9187', status: '\u72b6\u6001'
+      },
       form: {
         name: '',
         category: '',
@@ -306,12 +352,11 @@ export default {
     async loadList() {
       this.loading = true
       try {
-        const res = await getFoodList({
-          page: this.page,
-          size: this.size,
-          category: this.categoryFilter || undefined,
-          keyword: this.keyword || undefined
-        })
+        const sortParams = { page: this.page, size: this.size }
+        if (this.categoryFilter) sortParams.category = this.categoryFilter
+        if (this.keyword) sortParams.keyword = this.keyword
+        if (this.sortState.length > 0) sortParams.sort = this.sortState
+        const res = await getFoodList(sortParams)
         this.list = res.data && res.data.records || []
         this.total = res.data && res.data.total || 0
       } catch (e) {
@@ -372,6 +417,54 @@ export default {
       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' ? ' \u2191' : ' \u2193'
+    },
+    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) {
       this.size = val
       this.page = 1
@@ -524,4 +617,22 @@ export default {
 .el-descriptions {
   margin-top: 8px;
 }
+.sort-header {
+  cursor: pointer;
+  user-select: none;
+  font-weight: 500;
+  font-size: 12px;
+}
+.sort-header:hover {
+  color: #409eff;
+}
+.sort-asc ::v-deep .el-table__header-wrapper th .cell,
+.sort-desc ::v-deep .el-table__header-wrapper th .cell {
+  color: #409eff;
+  font-weight: 600;
+}
+.sort-tag {
+  margin-left: 8px;
+  cursor: pointer;
+}
 </style>