Forráskód Böngészése

Merge branch 'agriculture' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5 into agriculture

wangsisi 7 órája
szülő
commit
0675bfce98

+ 12 - 6
src/views/old_mini/agri_file/fileMap.js

@@ -245,13 +245,14 @@ function getUniqueExtents(features) {
     return extents;
 }
 
-function resolveFitExtent(features) {
+function resolveFitExtent(features, { bufferRatio = 0.35 } = {}) {
     const extents = getUniqueExtents(features);
     if (!extents.length) return null;
 
     const fitOne = (e) => {
+        if (!bufferRatio) return e.slice();
         const span = Math.max(getWidth(e), getHeight(e), 0.00001);
-        return bufferExtent(e, span * 0.35);
+        return bufferExtent(e, span * bufferRatio);
     };
 
     if (extents.length === 1) return fitOne(extents[0]);
@@ -261,6 +262,7 @@ function resolveFitExtent(features) {
     if (getWidth(union) > 0.15 || getHeight(union) > 0.15) {
         return fitOne(extents[0]);
     }
+    if (!bufferRatio) return union;
     return bufferExtent(union, Math.max(getWidth(union), getHeight(union), 0.00001) * 0.12);
 }
 
@@ -461,7 +463,7 @@ class FileMap {
         source.changed();
     }
 
-    fitView(renderToken) {
+    fitView(renderToken, options = {}) {
         if (!this.kmap?.map) return;
         if (renderToken != null && renderToken !== this._renderToken) return;
 
@@ -469,23 +471,27 @@ class FileMap {
         map.updateSize();
 
         const features = this.recordPolygonLayer.source.getFeatures();
-        const extent = resolveFitExtent(features);
+        const extent = resolveFitExtent(features, {
+            bufferRatio: options.bufferRatio ?? 0.35,
+        });
         if (!extent || isEmptyExtent(extent)) return;
 
         const size = map.getSize();
         if (!size || size[0] < 4 || size[1] < 4) {
-            this._fitTimer = setTimeout(() => this.fitView(renderToken), 120);
+            this._fitTimer = setTimeout(() => this.fitView(renderToken, options), 120);
             return;
         }
 
         const view = this.kmap.getView();
         view.cancelAnimations?.();
 
+        const padding = options.padding || this.fitPadding || [100, 40, 40, 40];
+
         // Tab 切换时不用动画,避免连续 fit 互相打断导致视图停在上一 Tab 区域
         view.fit(extent, {
             size,
             duration: 0,
-            padding: [100, 40, 40, 40],
+            padding,
             maxZoom: 19,
         });
         if ((view.getZoom() ?? 0) < 16) {

+ 11 - 10
src/views/old_mini/agri_file/pages/addZone.vue

@@ -21,13 +21,12 @@
         <RegionNamePopup v-model:show="showRegionNamePopup" @confirm="handleFarmNameConfirm" />
         <add-zone-info-popup v-model:show="showZoneInfoPopup" @confirm="handleZoneInfoConfirm" />
 
-        <tip-popup v-model:show="showExecuteSuccessPopup" type="executeSuccess" text="分区/相册已创建成功" hideBtn>
-            <template #footer>
+        <tip-popup v-model:show="showExecuteSuccessPopup" type="executeSuccess" text="分区/相册已创建成功" buttonText="完成" @confirm="handleComplete">
+            <!-- <template #footer>
                 <div class="bottom-btn-container">
                     <div class="bottom-btn secondary-btn" @click="handleComplete">完成</div>
-                    <div class="bottom-btn primary-btn" @click="handleContinueCreate">继续新建</div>
                 </div>
-            </template>
+            </template> -->
         </tip-popup>
 
         <tip-popup
@@ -207,10 +206,14 @@ function initMapView() {
     mapManage.initMap(getMapLocation(), mapContainer.value, {
         editable: true,
         zoneStyle: ZONE_STYLE,
+        singleDraw: true,
         preventOverlapExisting: isRegionDrawMode.value,
         onDrawOverlap: () => {
             ElMessage.warning("新绘制范围不能与已有品种种植范围重叠");
         },
+        onDrawLimit: () => {
+            ElMessage.warning("只能勾画一个范围,请先取消后再重新勾画");
+        },
     });
     mapManage.enableRegionDrawing();
     if (isFarmDrawMode.value) {
@@ -242,7 +245,11 @@ const handleLocate = () => {
 };
 
 const handleClearDraw = () => {
+    // 只清当前勾画,保留已有品种种植范围
     mapManage.clearLayer();
+    if (isRegionDrawMode.value) {
+        mapManage.setExistingVarietyZones(getMockVarietyZones());
+    }
 };
 
 const handleConfirmDraw = () => {
@@ -323,12 +330,6 @@ const handleComplete = () => {
     leaveAfterConfirm();
 }
 
-const handleContinueCreate = () => {
-    showExecuteSuccessPopup.value = false;
-    mapManage.clearLayer();
-    initMapView();
-}
-
 onActivated(() => {
     nextTick(() => initMapView());
 });

+ 2 - 0
src/views/old_mini/agri_file/pages/pestInteract.vue

@@ -159,6 +159,8 @@ function loadPlotLayers() {
         feature.set("zone_id", MOCK_ZONES[index]?.id);
     });
     selectedZoneId.value = MOCK_ZONES[0]?.id ?? null;
+    // padding=0 仍不贴边是因为默认还会给 extent 扩 35% 缓冲,这里关掉
+    fileMap.fitView(null, { padding: [60, 10, 20, 10], bufferRatio: 0 });
 }
 
 function onMapClick(evt) {

+ 5 - 1
src/views/old_mini/agri_file/pages/regionAlbums.vue

@@ -68,7 +68,7 @@
 </template>
 
 <script setup>
-import { computed, onActivated, onMounted, ref } from "vue";
+import { computed, onActivated, onDeactivated, onMounted, ref } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
 import { ArrowLeftBold, Plus } from "@element-plus/icons-vue";
@@ -300,6 +300,10 @@ onMounted(() => {
 onActivated(() => {
     syncAlbumByRoute();
 });
+
+onDeactivated(() => {
+    showPreview.value = false;
+});
 </script>
 
 <style lang="scss" scoped>

+ 2 - 39
src/views/old_mini/entry_information/components/addMachinePopup.vue

@@ -9,28 +9,11 @@
     >
         <div class="add-machine-popup__content">
             <div class="form-block">
-                <div class="form-block__title">农机类别</div>
-                <el-select
-                    v-model="form.categoryName"
-                    class="form-select"
-                    placeholder="请选择农机类别"
-                    clearable
-                >
-                    <el-option
-                        v-for="item in categoryOptions"
-                        :key="item"
-                        :label="item"
-                        :value="item"
-                    />
-                </el-select>
-            </div>
-
-            <div class="form-block">
                 <div class="form-block__title">农机名称</div>
                 <el-input v-model="form.name" placeholder="请输入农机名称" />
             </div>
 
-            <div class="add-machine-popup__confirm" @click="handleConfirm">确认区域</div>
+            <div class="add-machine-popup__confirm" @click="handleConfirm">确认</div>
         </div>
     </popup>
 </template>
@@ -45,16 +28,11 @@ const props = defineProps({
         type: Boolean,
         default: false,
     },
-    categoryOptions: {
-        type: Array,
-        default: () => [],
-    },
 });
 
 const emit = defineEmits(["update:show", "confirm"]);
 
 const form = reactive({
-    categoryName: "",
     name: "",
 });
 
@@ -67,24 +45,18 @@ watch(
     () => props.show,
     (val) => {
         if (val) {
-            form.categoryName = "";
             form.name = "";
         }
     }
 );
 
 const handleConfirm = () => {
-    const categoryName = String(form.categoryName || "").trim();
     const name = String(form.name || "").trim();
-    if (!categoryName) {
-        ElMessage.warning("请选择农机类别");
-        return;
-    }
     if (!name) {
         ElMessage.warning("请输入农机名称");
         return;
     }
-    emit("confirm", { categoryName, name });
+    emit("confirm", { name });
     emit("update:show", false);
 };
 </script>
@@ -106,10 +78,6 @@ const handleConfirm = () => {
     }
 
     .form-block {
-        & + .form-block {
-            margin-top: 16px;
-        }
-
         &__title {
             margin-bottom: 10px;
             font-size: 16px;
@@ -118,11 +86,6 @@ const handleConfirm = () => {
         }
     }
 
-    .form-select {
-        width: 100%;
-    }
-
-    :deep(.el-select__wrapper),
     :deep(.el-input__wrapper) {
         min-height: 40px;
         border-radius: 4px;

+ 31 - 18
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -63,7 +63,6 @@
 
         <add-machine-popup
             v-model:show="showAddPopup"
-            :category-options="categoryOptions"
             @confirm="handleAddConfirm"
         />
 
@@ -113,7 +112,7 @@ let customMachineId = 9000;
 
 const DEFAULT_GROUPS = [
     {
-        name: "水肥类",
+        name: "耕地(整地、施肥、撒肥、中耕等)",
         items: [
             { id: 101, name: "滴灌系统" },
             { id: 102, name: "喷灌设备" },
@@ -124,7 +123,7 @@ const DEFAULT_GROUPS = [
         ],
     },
     {
-        name: "类别一",
+        name: "种(播种、育秧、移栽、补苗等)",
         items: [
             { id: 201, name: "拖拉机" },
             { id: 202, name: "旋耕机" },
@@ -135,7 +134,7 @@ const DEFAULT_GROUPS = [
         ],
     },
     {
-        name: "类别二",
+        name: "收(收割、烘干、运输等)",
         items: [
             { id: 301, name: "修剪机" },
             { id: 302, name: "采收机" },
@@ -251,10 +250,6 @@ const displayGroups = computed(() => {
         .filter((group) => group.items.length);
 });
 
-const categoryOptions = computed(() =>
-    equipmentGroups.value.map((group) => group.name).filter(Boolean)
-);
-
 const allEquipmentItems = computed(() =>
     equipmentGroups.value.flatMap((group) => group.items)
 );
@@ -368,25 +363,40 @@ const handleAddMachine = () => {
     showAddPopup.value = true;
 };
 
-const handleAddConfirm = ({ categoryName, name }) => {
-    let group = equipmentGroups.value.find((item) => item.name === categoryName);
-    if (!group) {
-        group = { name: categoryName, items: [] };
-        equipmentGroups.value.push(group);
-    }
-    const exists = group.items.find((item) => item.name === name);
+const handleAddConfirm = ({ name }) => {
+    const machineName = String(name || "").trim();
+    if (!machineName) return;
+
+    const CUSTOM_GROUP_NAME = "我的农机";
+
+    // 已存在同名则直接选中
+    const exists = allEquipmentItems.value.find((item) => item.name === machineName);
     if (exists) {
         selectedIds.add(String(exists.id));
         saveSelectionDraft();
         ElMessage.success("已选中该农机");
         return;
     }
+
+    // 确保「自定义类」作为第一组存在
+    let customGroup = equipmentGroups.value.find((group) => group.name === CUSTOM_GROUP_NAME);
+    if (!customGroup) {
+        customGroup = { name: CUSTOM_GROUP_NAME, items: [] };
+        equipmentGroups.value.unshift(customGroup);
+    } else {
+        const index = equipmentGroups.value.indexOf(customGroup);
+        if (index > 0) {
+            equipmentGroups.value.splice(index, 1);
+            equipmentGroups.value.unshift(customGroup);
+        }
+    }
+
     const newItem = {
         id: ++customMachineId,
-        name,
+        name: machineName,
         custom: true,
     };
-    group.items.push(newItem);
+    customGroup.items.push(newItem);
     selectedIds.add(String(newItem.id));
     saveSelectionDraft();
     ElMessage.success("添加成功");
@@ -563,6 +573,9 @@ onMounted(() => {
             color: rgba(46, 46, 46, 0.4);
             line-height: 20px;
         }
+        .text-tip {
+            font-size: 20px;
+        }
     }
 
     .toolbar {
@@ -640,7 +653,7 @@ onMounted(() => {
         gap: 8px;
         margin-bottom: 4px;
         font-size: 16px;
-        font-weight: 600;
+        font-weight: 500;
         color: #1a1a1a;
 
         .title-icon {

+ 38 - 1
src/views/old_mini/recordDetails/map/mapManage.js

@@ -174,13 +174,17 @@ class MapManage {
       zoneStyle,
       preventOverlapExisting = false,
       onDrawOverlap,
+      singleDraw = false,
+      onDrawLimit,
     } = options;
     this.zoneStyle = zoneStyle ? { ...DEFAULT_ZONE_STYLE, ...zoneStyle } : { ...DEFAULT_ZONE_STYLE };
     this.editable = editable;
     this.constrainedDrawing = constrainedDrawing;
     this.preventOverlapExisting = !!preventOverlapExisting;
+    this.singleDraw = !!singleDraw;
     this.onDrawOutsideBoundary = typeof onDrawOutsideBoundary === "function" ? onDrawOutsideBoundary : null;
     this.onDrawOverlap = typeof onDrawOverlap === "function" ? onDrawOverlap : null;
+    this.onDrawLimit = typeof onDrawLimit === "function" ? onDrawLimit : null;
     this.constrainedDrawingReady = false;
     this.overlapConstraintReady = false;
     this.boundaryGeometry = null;
@@ -199,9 +203,11 @@ class MapManage {
         if (this.preventOverlapExisting && e?.feature) {
           this.handleDrawEndOverlap(e.feature);
         }
+        this.syncSingleDrawState();
       });
       this.kmap.modifyDraw();
       this.setupOverlapConstraintListeners();
+      this.setupSingleDrawListeners();
       this.setRegionDrawingActive(false);
     } else if (this.constrainedDrawing) {
       this.setupConstrainedDrawing();
@@ -209,6 +215,30 @@ class MapManage {
     }
   }
 
+  getDrawnFeatureCount() {
+    return this.kmap?.polygonLayer?.source?.getFeatures?.()?.length || 0;
+  }
+
+  /** 单范围模式:已有勾画则关闭继续绘制,仍可拖拽编辑 */
+  syncSingleDrawState() {
+    if (!this.singleDraw || !this.kmap?.draw) return;
+    const canDraw = this.regionDrawingActive && this.getDrawnFeatureCount() === 0;
+    this.kmap.draw.setActive(canDraw);
+  }
+
+  setupSingleDrawListeners() {
+    if (!this.kmap?.draw || !this.singleDraw) return;
+    this.kmap.draw.on("drawstart", () => {
+      if (!this.singleDraw) return;
+      if (this.getDrawnFeatureCount() === 0) return;
+      if (typeof this.kmap.draw.abortDrawing === "function") {
+        this.kmap.draw.abortDrawing();
+      }
+      this.onDrawLimit?.();
+      this.syncSingleDrawState();
+    });
+  }
+
   notifyDrawOverlap() {
     this.onDrawOverlap?.();
   }
@@ -584,7 +614,11 @@ class MapManage {
     this.regionDrawingActive = active;
     setViewportInteractionsActive(this.kmap.map, active);
     if (this.kmap.draw) {
-      this.kmap.draw.setActive(active);
+      if (active && this.singleDraw) {
+        this.syncSingleDrawState();
+      } else {
+        this.kmap.draw.setActive(active);
+      }
     }
     if (this.kmap.modify) {
       this.kmap.modify.setActive(active);
@@ -701,6 +735,7 @@ class MapManage {
     }
     this.kmap.polygonLayer.source.clear();
     this.clearGridLayer();
+    this.syncSingleDrawState();
   }
 
   clearAllLayers() {
@@ -1035,9 +1070,11 @@ class MapManage {
     this.constrainedDrawingReady = false;
     this.overlapConstraintReady = false;
     this.preventOverlapExisting = false;
+    this.singleDraw = false;
     this.boundaryGeometry = null;
     this.onDrawOutsideBoundary = null;
     this.onDrawOverlap = null;
+    this.onDrawLimit = null;
     this.selectedGridIds?.clear();
   }
 

+ 196 - 118
src/views/old_mini/work_execute/surveyEntry.vue

@@ -6,7 +6,7 @@
             <div class="hero">
                 <div class="hero-l">
                     <div class="hero__text">{{ t("workExecute.surveyEntryHint") }}</div>
-                    <div class="hero__text">{{ t("workExecute.surveyEntryHint2") }}</div>
+                    <div class="hero__text">{{ t("workExecute.surveyEntryHint2") }}<span class="text-tip">(可多选)</span></div>
                 </div>
                 <img class="hero__illust" src="@/assets/img/agricultural/enter.png" alt="" />
             </div>
@@ -23,22 +23,27 @@
                 <div class="search-bar__btn" @click="handleSearch">{{ t("workExecute.surveySearch") }}</div>
             </div>
 
-            <div class="category-panel">
-                <div class="category-panel__title">{{ t("workExecute.surveySelectTitle") }}<span class="category-panel__hint">({{ t("workExecute.surveySelectHint") }})</span></div>
-                <div class="category-grid" v-loading="loading">
-                    <div
-                        v-for="item in displayList"
-                        :key="item.id"
-                        class="category-tag"
-                        :class="{ selected: selectedIds.has(String(item.id)) }"
-                        @click="toggleSelect(item)"
-                    >
-                        <span class="category-tag__text van-ellipsis">{{ item.name }}</span>
+            <div class="category-panel" v-loading="loading">
+                <div v-for="group in displayGroups" :key="group.name" class="category-card">
+                    <div class="section-title">
+                        <span class="title-icon"></span>
+                        <span>{{ group.name }}</span>
                     </div>
-                    <div v-if="!loading && !displayList.length" class="empty-tip">
-                        {{ t("workExecute.surveyNoMatch") }}
+                    <div class="tag-group">
+                        <div
+                            v-for="item in group.items"
+                            :key="item.id"
+                            class="tag-item"
+                            :class="{ selected: selectedIds.has(String(item.id)) }"
+                            @click="toggleSelect(item)"
+                        >
+                            <span class="text">{{ item.name }}</span>
+                        </div>
                     </div>
                 </div>
+                <div v-if="!loading && !displayGroups.length" class="empty-tip">
+                    {{ t("workExecute.surveyNoMatch") }}
+                </div>
             </div>
         </div>
 
@@ -62,55 +67,99 @@ const router = useRouter();
 const SESSION_KEY = "SURVEY_SELECTED_WORK_TYPES";
 
 const loading = ref(false);
-const categoryList = ref([]);
+const categoryGroups = ref([]);
 const searchKeyword = ref("");
 const appliedKeyword = ref("");
 const selectedIds = reactive(new Set());
 
-const DEFAULT_CATEGORIES = [
-    "施肥管理",
-    "病虫害防治",
-    "灌溉补水",
-    "修剪整枝",
-    "疏花疏果",
-    "土壤改良",
-    "除草清园",
-    "采收作业",
-    "套袋护果",
-    "授粉辅助",
-    "果园巡查",
-    "气象防护",
-    "营养诊断",
-    "物候监测",
-    "农机作业",
-    "药肥配送",
-    "无人机植保",
-    "产后处理",
-    "仓储管理",
-    "其他农事",
-    "标准施肥",
-    "标准防治",
-    "标准调节",
-    "机动农事",
-    "预警响应",
-    "恢复农事",
-    "异常处置",
-].map((name, index) => ({ id: index + 1, name }));
-
-const displayList = computed(() => {
+const DEFAULT_GROUPS = [
+    {
+        name: "建园与土壤管理",
+        items: [
+            { id: 101, name: "整地类" },
+            { id: 102, name: "除草" },
+            { id: 103, name: "清园" },
+            { id: 104, name: "清沟修渠" },
+        ],
+    },
+    {
+        name: "肥水管理",
+        items: [
+            { id: 201, name: "根部肥" },
+            { id: 202, name: "根部灌水" },
+            { id: 203, name: "根部灌药" },
+            { id: 204, name: "飞防叶面肥" },
+            { id: 205, name: "叶面喷水" },
+        ],
+    },
+    {
+        name: "打药管理",
+        items: [
+            { id: 301, name: "飞防打药" },
+            { id: 302, name: "内膛打药" },
+        ],
+    },
+    {
+        name: "树体管理",
+        items: [
+            { id: 401, name: "整形修剪" },
+            { id: 402, name: "嫁接" },
+            { id: 403, name: "环割环剥" },
+        ],
+    },
+    {
+        name: "花果管理",
+        items: [
+            { id: 501, name: "疏花疏果" },
+            { id: 502, name: "果实套袋" },
+        ],
+    },
+];
+
+const displayGroups = computed(() => {
     const keyword = (appliedKeyword.value || "").trim();
-    if (!keyword) return categoryList.value;
-    return categoryList.value.filter((item) => item.name.includes(keyword));
+    if (!keyword) return categoryGroups.value;
+    return categoryGroups.value
+        .map((group) => ({
+            ...group,
+            items: group.items.filter((item) => item.name.includes(keyword)),
+        }))
+        .filter((group) => group.items.length);
 });
 
-const normalizeList = (list) => {
-    if (!Array.isArray(list)) return [];
-    return list
-        .map((item, index) => ({
+const allCategoryItems = computed(() =>
+    categoryGroups.value.flatMap((group) => group.items)
+);
+
+const normalizeGroups = (list) => {
+    if (!Array.isArray(list) || !list.length) return [];
+    // 已是分组结构
+    if (list[0]?.items) {
+        return list
+            .map((group, gIndex) => ({
+                name: group.name || group.category_name || `分类${gIndex + 1}`,
+                items: (group.items || group.list || [])
+                    .map((item, index) => ({
+                        id: item.id ?? item.type ?? item.code ?? `${gIndex}-${index + 1}`,
+                        name: item.name || item.type_name || item.label || "",
+                    }))
+                    .filter((item) => item.name),
+            }))
+            .filter((group) => group.items.length);
+    }
+    // 扁平列表兜底:按 category / group 字段分组
+    const map = new Map();
+    list.forEach((item, index) => {
+        const name = item.name || item.type_name || item.label || "";
+        if (!name) return;
+        const groupName = item.category_name || item.group_name || item.category || "其他";
+        if (!map.has(groupName)) map.set(groupName, []);
+        map.get(groupName).push({
             id: item.id ?? item.type ?? item.code ?? index + 1,
-            name: item.name || item.type_name || item.label || "",
-        }))
-        .filter((item) => item.name);
+            name,
+        });
+    });
+    return Array.from(map.entries()).map(([name, items]) => ({ name, items }));
 };
 
 const restoreSelection = () => {
@@ -127,12 +176,12 @@ const restoreSelection = () => {
 const fetchCategories = async () => {
     loading.value = true;
     try {
-        categoryList.value = DEFAULT_CATEGORIES;
+        categoryGroups.value = DEFAULT_GROUPS;
         // const res = await VE_API.z_farm_work_record.getFarmWorkTypeList();
-        // const list = normalizeList(res?.data);
-        // categoryList.value = list.length ? list : DEFAULT_CATEGORIES;
+        // const groups = normalizeGroups(res?.data);
+        // categoryGroups.value = groups.length ? groups : DEFAULT_GROUPS;
     } catch {
-        categoryList.value = DEFAULT_CATEGORIES;
+        categoryGroups.value = DEFAULT_GROUPS;
     } finally {
         loading.value = false;
     }
@@ -152,7 +201,7 @@ const toggleSelect = (item) => {
 };
 
 const handleSubmit = () => {
-    const selected = categoryList.value.filter((item) => selectedIds.has(String(item.id)));
+    const selected = allCategoryItems.value.filter((item) => selectedIds.has(String(item.id)));
     if (!selected.length) {
         ElMessage.warning(t("workExecute.surveyPleaseSelect"));
         return;
@@ -181,7 +230,7 @@ onMounted(() => {
 }
 
 .survey-entry-body {
-    padding: 0 10px 10px 10px;
+    padding: 0 10px;
     flex: 1;
     min-height: 0;
     display: flex;
@@ -200,6 +249,9 @@ onMounted(() => {
         line-height: 24px;
         color: #005599;
         font-family: "PangMenZhengDao";
+        &.text-tip {
+            font-size: 16px;
+        }
     }
 
     &__illust {
@@ -258,81 +310,107 @@ onMounted(() => {
 .category-panel {
     flex: 1;
     min-height: 0;
-    display: flex;
-    flex-direction: column;
-    padding: 12px 12px 0;
-    border-radius: 10px;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+    padding-bottom: 8px;
+}
+
+.category-card {
     background: #fff;
-    overflow: hidden;
+    border-radius: 12px;
+    padding: 14px 12px 16px;
+    margin-bottom: 12px;
+}
 
-    &__title {
-        padding-bottom: 10px;
-        margin-bottom: 10px;
-        font-size: 16px;
-        font-weight: 500;
-        color: #222222;
-        border-bottom: 0.2px solid rgba(0, 0, 0, 0.1);
-    }
+.section-title {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 4px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #1a1a1a;
+
+    .title-icon {
+        position: relative;
+        width: 14px;
+        height: 14px;
+        flex-shrink: 0;
 
-    &__hint {
-        font-size: 14px;
-        color: rgba(34, 34, 34, 0.5);
+        &::before,
+        &::after {
+            content: "";
+            position: absolute;
+            width: 10px;
+            height: 10px;
+            border-radius: 50%;
+        }
+
+        &::before {
+            left: 0;
+            top: 2px;
+            background: #2199f8;
+            opacity: 0.85;
+        }
+
+        &::after {
+            right: 0;
+            top: 0;
+            background: #7ec8ff;
+            opacity: 0.9;
+        }
     }
 }
 
-.category-grid {
-    flex: 1;
-    min-height: 0;
-    overflow-y: auto;
+.tag-group {
     display: grid;
     grid-template-columns: repeat(3, 1fr);
-    gap: 10px;
-    align-content: start;
-    padding-bottom: 16px;
-}
-
-.category-tag {
-    position: relative;
-    height: 40px;
-    padding: 0 8px;
-    border-radius: 8px;
-    background: #f3f4f6;
-    border: 1px solid transparent;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    box-sizing: border-box;
-    cursor: pointer;
+    gap: 0 8px;
+    font-size: 14px;
 
-    &__text {
-        max-width: 100%;
-        font-size: 14px;
-        color: #1a1a1a;
+    .tag-item {
+        margin-top: 10px;
+        position: relative;
+        border-radius: 6px;
+        box-sizing: border-box;
+        height: 36px;
         text-align: center;
-    }
-
-    &.selected {
-        background: #fff;
-        border-color: #2199f8;
+        line-height: 36px;
+        cursor: pointer;
+        color: #000;
+        background: #f3f4f6;
+        border: 1px solid transparent;
+
+        .text {
+            display: inline-block;
+            max-width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            vertical-align: top;
+            padding: 0 6px;
+        }
 
-        .category-tag__text {
+        &.selected {
+            border: 1px solid #2199f8;
+            background: #e8f5ff;
             color: #2199f8;
-        }
 
-        &::after {
-            content: "";
-            position: absolute;
-            top: -1px;
-            right: -1px;
-            width: 18px;
-            height: 14px;
-            background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+            &::after {
+                content: "";
+                position: absolute;
+                z-index: 9;
+                top: -1px;
+                right: -1px;
+                width: 18px;
+                height: 14px;
+                background: url("@/assets/img/home/checked-bg-top.png") no-repeat bottom right / 18px 13px;
+            }
         }
     }
 }
 
 .empty-tip {
-    grid-column: 1 / -1;
     padding: 48px 0;
     text-align: center;
     color: rgba(0, 0, 0, 0.35);