瀏覽代碼

花掌柜-商品分类设置优化

ouyang 3 周之前
父節點
當前提交
abf1c29b62

+ 485 - 485
hdApp/src/admin/goods/ad-goods-select.vue

@@ -1,486 +1,486 @@
-<!--

-  分类广告 - 商品多选页

-  用途:从花束商品中勾选关联商品,返回商品 id 列表给 category-ad

-  基于 manage 花束面板精简:无打标签/库存/编辑/更多/新增,无花材标签筛选

--->

-<template>

-  <view class="goods-select-page">

-    <view class="search-row">

-      <view class="search-type-wrap">

-        <button class="type-btn" :class="{ active: searchType === 0 }" hover-class="none" @click="changeSearchType(0)">名称</button>

-        <button class="type-btn" :class="{ active: searchType === 1 }" hover-class="none" @click="changeSearchType(1)">编号</button>

-      </view>

-      <view class="search-box">

-        <input v-model="searchText" class="search-input" placeholder="请输入" confirm-type="search" @confirm="searchGoods" />

-      </view>

-      <button class="search-btn" hover-class="none" @click="searchGoods">搜索</button>

-    </view>

-

-    <view class="filter-row">

-      <view class="filter-tabs">

-        <view class="filter-tab" :class="{ active: priceType === -1 }" @click="switchPriceTab(-1)">全部</view>

-        <view class="filter-tab" :class="{ active: priceType === 0 }" @click="switchPriceTab(0)">无价</view>

-        <view class="filter-tab" :class="{ active: priceType === 1 }" @click="switchPriceTab(1)">有价</view>

-      </view>

-      <button class="refresh-btn" hover-class="none" @click="refreshGoods">刷新</button>

-    </view>

-

-    <view class="content-area">

-      <scroll-view scroll-y class="category-list">

-        <view

-          v-for="(item, index) in tabbar"

-          :key="item.id || index"

-          class="category-item"

-          :class="{ active: currentTab === index }"

-          :data-current="index"

-          @tap.stop="switchCategory($event, item)"

-        >

-          <text>{{ item.categoryName || '' }}</text>

-        </view>

-      </scroll-view>

-

-      <scroll-view scroll-y class="list-box" @scrolltolower="goodsReachBottom">

-        <block v-if="!$util.isEmpty(list.data)">

-          <view

-            class="goods-row"

-            v-for="(goodsItem, idx) in list.data"

-            :key="goodsItem.id || idx"

-            :data-id="goodsItem.id"

-            @tap.stop="toggleGoodsByEvent"

-          >

-            <view class="check-box" :class="{ checked: !!selectedMap[String(goodsItem.id)] }">

-              <text v-if="selectedMap[String(goodsItem.id)]" class="check-mark">✓</text>

-            </view>

-            <image class="goods-img" :src="goodsItem.smallCover" mode="aspectFill" />

-            <view class="goods-info">

-              <view class="goods-name">{{ goodsItem.name || '' }}</view>

-              <view class="goods-meta">

-                <text v-if="Number(goodsItem.sn) > 0">#{{ goodsItem.sn }}</text>

-                <text class="price" v-if="goodsItem.priceType == 1">¥{{ goodsItem.price ? parseFloat(goodsItem.price) : 0 }}</text>

-                <text class="price" v-else>---</text>

-              </view>

-            </view>

-          </view>

-        </block>

-        <app-wrapper-empty v-else title="暂无商品" :is-empty="$util.isEmpty(list.data)" />

-      </scroll-view>

-    </view>

-

-    <view class="footer-bar">

-      <text class="selected-tip">已选 {{ selectedCount }} 个</text>

-      <button class="confirm-btn" hover-class="none" @click="confirmSelect">确定</button>

-    </view>

-  </view>

-</template>

-

-<script>

-import AppWrapperEmpty from '@/components/app-wrapper-empty';

-import { list } from '@/mixins';

-import { categoryListInfo, getCategoryToGoods } from '@/api/goods';

-

-export default {

-  name: 'ad-goods-select',

-  components: { AppWrapperEmpty },

-  mixins: [list],

-  data() {

-    return {

-      slotIndex: 0,

-      // 用对象存选中态,小程序端 $set 比数组 splice 更易触发视图更新

-      selectedMap: {},

-      tabbar: [],

-      currentTab: 0,

-      catId: '',

-      catName: '',

-      searchText: '',

-      searchType: 0,

-      lastSearchText: '',

-      priceType: -1,

-      hasStock: -1,

-      autoLoad: false,

-      goodsSelectPageReady: false,

-      selectedIdsParsed: false

-    };

-  },

-  computed: {

-    /** 已选商品数量,供底部栏展示 */

-    selectedCount() {

-      return Object.keys(this.selectedMap).length;

-    }

-  },

-  onLoad(options) {

-    // 兜底解析路由参数;主流程由 globalMixins 触发的 init 执行

-    this.applyPageOptions(options || {});

-    this.ensureLoadCategories();

-  },

-  methods: {

-    /** globalMixins 在 onLoad 会调用 init,需在此拉取分类与商品 */

-    init() {

-      this.applyPageOptions(this.option || {});

-      this.ensureLoadCategories();

-    },

-    /** 解析 slotIndex、已选商品 id,可重复调用但 selectedIds 只回填一次 */

-    applyPageOptions(options) {

-      if (options.slotIndex !== undefined && options.slotIndex !== '') {

-        this.slotIndex = parseInt(options.slotIndex || 0, 10);

-      }

-      if (!this.selectedIdsParsed && options.selectedIds) {

-        this.selectedIdsParsed = true;

-        String(options.selectedIds)

-          .split(',')

-          .filter(Boolean)

-          .forEach((id) => {

-            this.$set(this.selectedMap, String(id), true);

-          });

-      }

-    },

-    /** 避免 init / onLoad 重复请求分类列表 */

-    ensureLoadCategories() {

-      if (this.goodsSelectPageReady) return;

-      this.goodsSelectPageReady = true;

-      this.loadCategories();

-    },

-    /** 只取花束分类 flower=0 */

-    loadCategories() {

-      categoryListInfo().then((res) => {

-        const all = res.data || [];

-        this.tabbar = all.filter((item) => item.flower == 0);

-        if (this.tabbar.length && this.tabbar[0].id) {

-          this.catId = this.tabbar[0].id;

-          this.catName = this.tabbar[0].categoryName;

-          this.refreshGoods();

-        }

-      });

-    },

-    getGoodsList() {

-      return getCategoryToGoods({

-        page: this.list.page,

-        searchText: this.lastSearchText,

-        searchType: this.searchType,

-        catId: this.catId,

-        flowerNum: 0,

-        priceType: this.priceType,

-        hasStock: this.hasStock,

-        requestType: 'goodsList'

-      }).then((res) => {

-        this.completes(res);

-      });

-    },

-    refreshGoods() {

-      this.resetList();

-      this.getGoodsList();

-    },

-    goodsReachBottom() {

-      if (!this.list.finished) {

-        this.getGoodsList();

-      }

-    },

-    /** 切换左侧分类;index 从 data-current 读取,兼容小程序 tap 传参 */

-    switchCategory(e, item) {

-      const index = Number(e.currentTarget.dataset.current);

-      const category = item || this.tabbar[index];

-      if (!category || category.id === undefined || category.id === null) return;

-      if (this.currentTab === index) return;

-      this.currentTab = index;

-      this.catId = category.id;

-      this.catName = category.categoryName || '';

-      this.searchText = '';

-      this.lastSearchText = '';

-      this.refreshGoods();

-    },

-    changeSearchType(i) {

-      this.searchType = i;

-      this.searchGoods();

-    },

-    searchGoods() {

-      this.lastSearchText = this.searchText;

-      if (!this.$util.isEmpty(this.lastSearchText)) {

-        this.catId = 0;

-        this.catName = '花束';

-        this.currentTab = -1;

-      } else if (this.tabbar[0]) {

-        this.currentTab = 0;

-        this.catId = this.tabbar[0].id;

-        this.catName = this.tabbar[0].categoryName;

-      }

-      this.refreshGoods();

-    },

-    switchPriceTab(value) {

-      if (this.priceType === value) return;

-      this.priceType = value;

-      this.refreshGoods();

-    },

-    /** 从 data-id 读取商品 id,避免小程序 tap 回调参数错位 */

-    toggleGoodsByEvent(e) {

-      const id = e.currentTarget.dataset.id;

-      if (id === undefined || id === null || id === '') return;

-      const key = String(id);

-      if (this.selectedMap[key]) {

-        this.$delete(this.selectedMap, key);

-      } else {

-        this.$set(this.selectedMap, key, true);

-      }

-    },

-    confirmSelect() {

-      uni.$emit('categoryAdGoodsSelected', {

-        slotIndex: this.slotIndex,

-        goodsIds: Object.keys(this.selectedMap)

-      });

-      uni.navigateBack();

-    }

-  }

-};

-</script>

-

-<style lang="scss" scoped>

-.goods-select-page {

-  height: 100vh;

-  display: flex;

-  flex-direction: column;

-  background: #fff;

-}

-

-.search-row {

-  display: flex;

-  align-items: center;

-  padding: 16rpx 18rpx;

-  flex-shrink: 0;

-}

-

-.search-type-wrap {

-  display: flex;

-  margin-right: 10rpx;

-}

-

-.type-btn {

-  width: 92rpx;

-  height: 64rpx;

-  line-height: 64rpx;

-  padding: 0;

-  margin: 0 8rpx 0 0;

-  font-size: 24rpx;

-  background: #f5f5f5;

-  color: #333;

-  border-radius: 8rpx;

-}

-

-.type-btn::after {

-  border: none;

-}

-

-.type-btn.active {

-  background: #52c41a;

-  color: #fff;

-}

-

-.search-box {

-  flex: 1;

-  height: 64rpx;

-  border: 2rpx solid #e2e2e2;

-  border-radius: 34rpx;

-  padding: 0 24rpx;

-  box-sizing: border-box;

-  min-width: 0;

-  margin-right: 10rpx;

-}

-

-.search-input {

-  height: 60rpx;

-  font-size: 26rpx;

-}

-

-.search-btn {

-  width: 108rpx;

-  height: 64rpx;

-  line-height: 64rpx;

-  padding: 0;

-  margin: 0;

-  font-size: 26rpx;

-  background: #52c41a;

-  color: #fff;

-  border-radius: 8rpx;

-}

-

-.search-btn::after {

-  border: none;

-}

-

-.filter-row {

-  display: flex;

-  align-items: center;

-  justify-content: space-between;

-  padding: 0 18rpx 16rpx;

-  flex-shrink: 0;

-}

-

-.filter-tabs {

-  display: flex;

-}

-

-.filter-tab {

-  margin-right: 40rpx;

-  font-size: 26rpx;

-  color: #111;

-  font-weight: 600;

-  position: relative;

-}

-

-.filter-tab.active {

-  color: #22a500;

-}

-

-.filter-tab.active::after {

-  content: '';

-  position: absolute;

-  left: 0;

-  right: 0;

-  bottom: -6rpx;

-  height: 4rpx;

-  background: #22a500;

-}

-

-.refresh-btn {

-  height: 56rpx;

-  line-height: 56rpx;

-  padding: 0 20rpx;

-  margin: 0;

-  font-size: 24rpx;

-  background: #f5f5f5;

-  color: #333;

-  border-radius: 8rpx;

-}

-

-.refresh-btn::after {

-  border: none;

-}

-

-.content-area {

-  flex: 1;

-  min-height: 0;

-  display: flex;

-  overflow: hidden;

-}

-

-.category-list {

-  width: 202rpx;

-  height: 100%;

-  flex-shrink: 0;

-  background: #fafafa;

-}

-

-.category-item {

-  min-height: 92rpx;

-  padding: 18rpx 10rpx 18rpx 24rpx;

-  font-size: 24rpx;

-  color: #111;

-  font-weight: 600;

-}

-

-.category-item.active {

-  color: #189b00;

-  background: #fff;

-}

-

-.list-box {

-  flex: 1;

-  height: 100%;

-  min-width: 0;

-  padding: 0 18rpx 120rpx;

-  box-sizing: border-box;

-}

-

-.goods-row {

-  display: flex;

-  align-items: center;

-  padding: 20rpx 0;

-  border-bottom: 1rpx solid #f0f0f0;

-}

-

-.check-box {

-  width: 36rpx;

-  height: 36rpx;

-  border: 2rpx solid #ccc;

-  border-radius: 6rpx;

-  margin-right: 16rpx;

-  flex-shrink: 0;

-  display: flex;

-  align-items: center;

-  justify-content: center;

-}

-

-.check-box.checked {

-  background: #52c41a;

-  border-color: #52c41a;

-}

-

-.check-mark {

-  color: #fff;

-  font-size: 24rpx;

-}

-

-.goods-img {

-  width: 120rpx;

-  height: 120rpx;

-  border-radius: 8rpx;

-  margin-right: 16rpx;

-  flex-shrink: 0;

-}

-

-.goods-info {

-  flex: 1;

-  min-width: 0;

-}

-

-.goods-name {

-  font-size: 28rpx;

-  color: #333;

-  line-height: 1.4;

-  margin-bottom: 8rpx;

-}

-

-.goods-meta {

-  font-size: 24rpx;

-  color: #999;

-}

-

-.price {

-  margin-left: 12rpx;

-  color: #e4393c;

-}

-

-.footer-bar {

-  position: fixed;

-  left: 0;

-  right: 0;

-  bottom: 0;

-  display: flex;

-  align-items: center;

-  justify-content: space-between;

-  padding: 20rpx 30rpx;

-  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));

-  background: #fff;

-  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.06);

-  z-index: 10;

-}

-

-.selected-tip {

-  font-size: 28rpx;

-  color: #666;

-}

-

-.confirm-btn {

-  width: 240rpx;

-  height: 72rpx;

-  line-height: 72rpx;

-  margin: 0;

-  padding: 0;

-  background: #52c41a;

-  color: #fff;

-  font-size: 28rpx;

-  border-radius: 8rpx;

-}

-

-.confirm-btn::after {

-  border: none;

-}

-</style>

+<!--
+  分类广告 - 商品多选页
+  用途:从花束商品中勾选关联商品,返回商品 id 列表给 category-ad
+  基于 manage 花束面板精简:无打标签/库存/编辑/更多/新增,无花材标签筛选
+-->
+<template>
+  <view class="goods-select-page">
+    <view class="search-row">
+      <view class="search-type-wrap">
+        <button class="type-btn" :class="{ active: searchType === 0 }" hover-class="none" @click="changeSearchType(0)">名称</button>
+        <button class="type-btn" :class="{ active: searchType === 1 }" hover-class="none" @click="changeSearchType(1)">编号</button>
+      </view>
+      <view class="search-box">
+        <input v-model="searchText" class="search-input" placeholder="请输入" confirm-type="search" @confirm="searchGoods" />
+      </view>
+      <button class="search-btn" hover-class="none" @click="searchGoods">搜索</button>
+    </view>
+
+    <view class="filter-row">
+      <view class="filter-tabs">
+        <view class="filter-tab" :class="{ active: priceType === -1 }" @click="switchPriceTab(-1)">全部</view>
+        <view class="filter-tab" :class="{ active: priceType === 0 }" @click="switchPriceTab(0)">无价</view>
+        <view class="filter-tab" :class="{ active: priceType === 1 }" @click="switchPriceTab(1)">有价</view>
+      </view>
+      <button class="refresh-btn" hover-class="none" @click="refreshGoods">刷新</button>
+    </view>
+
+    <view class="content-area">
+      <scroll-view scroll-y class="category-list">
+        <view
+          v-for="(item, index) in tabbar"
+          :key="item.id || index"
+          class="category-item"
+          :class="{ active: currentTab === index }"
+          :data-current="index"
+          @tap.stop="switchCategory($event, item)"
+        >
+          <text>{{ item.categoryName || '' }}</text>
+        </view>
+      </scroll-view>
+
+      <scroll-view scroll-y class="list-box" @scrolltolower="goodsReachBottom">
+        <block v-if="!$util.isEmpty(list.data)">
+          <view
+            class="goods-row"
+            v-for="(goodsItem, idx) in list.data"
+            :key="goodsItem.id || idx"
+            :data-id="goodsItem.id"
+            @tap.stop="toggleGoodsByEvent"
+          >
+            <view class="check-box" :class="{ checked: !!selectedMap[String(goodsItem.id)] }">
+              <text v-if="selectedMap[String(goodsItem.id)]" class="check-mark">✓</text>
+            </view>
+            <image class="goods-img" :src="goodsItem.smallCover" mode="aspectFill" />
+            <view class="goods-info">
+              <view class="goods-name">{{ goodsItem.name || '' }}</view>
+              <view class="goods-meta">
+                <text v-if="Number(goodsItem.sn) > 0">#{{ goodsItem.sn }}</text>
+                <text class="price" v-if="goodsItem.priceType == 1">¥{{ goodsItem.price ? parseFloat(goodsItem.price) : 0 }}</text>
+                <text class="price" v-else>---</text>
+              </view>
+            </view>
+          </view>
+        </block>
+        <app-wrapper-empty v-else title="暂无商品" :is-empty="$util.isEmpty(list.data)" />
+      </scroll-view>
+    </view>
+
+    <view class="footer-bar">
+      <text class="selected-tip">已选 {{ selectedCount }} 个</text>
+      <button class="confirm-btn" hover-class="none" @click="confirmSelect">确定</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import AppWrapperEmpty from '@/components/app-wrapper-empty';
+import { list } from '@/mixins';
+import { categoryListInfo, getCategoryToGoods } from '@/api/goods';
+
+export default {
+  name: 'ad-goods-select',
+  components: { AppWrapperEmpty },
+  mixins: [list],
+  data() {
+    return {
+      slotIndex: 0,
+      // 用对象存选中态,小程序端 $set 比数组 splice 更易触发视图更新
+      selectedMap: {},
+      tabbar: [],
+      currentTab: 0,
+      catId: '',
+      catName: '',
+      searchText: '',
+      searchType: 0,
+      lastSearchText: '',
+      priceType: -1,
+      hasStock: -1,
+      autoLoad: false,
+      goodsSelectPageReady: false,
+      selectedIdsParsed: false
+    };
+  },
+  computed: {
+    /** 已选商品数量,供底部栏展示 */
+    selectedCount() {
+      return Object.keys(this.selectedMap).length;
+    }
+  },
+  onLoad(options) {
+    // 兜底解析路由参数;主流程由 globalMixins 触发的 init 执行
+    this.applyPageOptions(options || {});
+    this.ensureLoadCategories();
+  },
+  methods: {
+    /** globalMixins 在 onLoad 会调用 init,需在此拉取分类与商品 */
+    init() {
+      this.applyPageOptions(this.option || {});
+      this.ensureLoadCategories();
+    },
+    /** 解析 slotIndex、已选商品 id,可重复调用但 selectedIds 只回填一次 */
+    applyPageOptions(options) {
+      if (options.slotIndex !== undefined && options.slotIndex !== '') {
+        this.slotIndex = parseInt(options.slotIndex || 0, 10);
+      }
+      if (!this.selectedIdsParsed && options.selectedIds) {
+        this.selectedIdsParsed = true;
+        String(options.selectedIds)
+          .split(',')
+          .filter(Boolean)
+          .forEach((id) => {
+            this.$set(this.selectedMap, String(id), true);
+          });
+      }
+    },
+    /** 避免 init / onLoad 重复请求分类列表 */
+    ensureLoadCategories() {
+      if (this.goodsSelectPageReady) return;
+      this.goodsSelectPageReady = true;
+      this.loadCategories();
+    },
+    /** 花束分类来自 xhCategory,接口返回即为花束分类列表 */
+    loadCategories() {
+      categoryListInfo().then((res) => {
+        const all = res.data || [];
+        this.tabbar = all;
+        if (this.tabbar.length && this.tabbar[0].id) {
+          this.catId = this.tabbar[0].id;
+          this.catName = this.tabbar[0].categoryName;
+          this.refreshGoods();
+        }
+      });
+    },
+    getGoodsList() {
+      return getCategoryToGoods({
+        page: this.list.page,
+        searchText: this.lastSearchText,
+        searchType: this.searchType,
+        catId: this.catId,
+        flowerNum: 0,
+        priceType: this.priceType,
+        hasStock: this.hasStock,
+        requestType: 'goodsList'
+      }).then((res) => {
+        this.completes(res);
+      });
+    },
+    refreshGoods() {
+      this.resetList();
+      this.getGoodsList();
+    },
+    goodsReachBottom() {
+      if (!this.list.finished) {
+        this.getGoodsList();
+      }
+    },
+    /** 切换左侧分类;index 从 data-current 读取,兼容小程序 tap 传参 */
+    switchCategory(e, item) {
+      const index = Number(e.currentTarget.dataset.current);
+      const category = item || this.tabbar[index];
+      if (!category || category.id === undefined || category.id === null) return;
+      if (this.currentTab === index) return;
+      this.currentTab = index;
+      this.catId = category.id;
+      this.catName = category.categoryName || '';
+      this.searchText = '';
+      this.lastSearchText = '';
+      this.refreshGoods();
+    },
+    changeSearchType(i) {
+      this.searchType = i;
+      this.searchGoods();
+    },
+    searchGoods() {
+      this.lastSearchText = this.searchText;
+      if (!this.$util.isEmpty(this.lastSearchText)) {
+        this.catId = 0;
+        this.catName = '花束';
+        this.currentTab = -1;
+      } else if (this.tabbar[0]) {
+        this.currentTab = 0;
+        this.catId = this.tabbar[0].id;
+        this.catName = this.tabbar[0].categoryName;
+      }
+      this.refreshGoods();
+    },
+    switchPriceTab(value) {
+      if (this.priceType === value) return;
+      this.priceType = value;
+      this.refreshGoods();
+    },
+    /** 从 data-id 读取商品 id,避免小程序 tap 回调参数错位 */
+    toggleGoodsByEvent(e) {
+      const id = e.currentTarget.dataset.id;
+      if (id === undefined || id === null || id === '') return;
+      const key = String(id);
+      if (this.selectedMap[key]) {
+        this.$delete(this.selectedMap, key);
+      } else {
+        this.$set(this.selectedMap, key, true);
+      }
+    },
+    confirmSelect() {
+      uni.$emit('categoryAdGoodsSelected', {
+        slotIndex: this.slotIndex,
+        goodsIds: Object.keys(this.selectedMap)
+      });
+      uni.navigateBack();
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.goods-select-page {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+}
+
+.search-row {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 18rpx;
+  flex-shrink: 0;
+}
+
+.search-type-wrap {
+  display: flex;
+  margin-right: 10rpx;
+}
+
+.type-btn {
+  width: 92rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  padding: 0;
+  margin: 0 8rpx 0 0;
+  font-size: 24rpx;
+  background: #f5f5f5;
+  color: #333;
+  border-radius: 8rpx;
+}
+
+.type-btn::after {
+  border: none;
+}
+
+.type-btn.active {
+  background: #52c41a;
+  color: #fff;
+}
+
+.search-box {
+  flex: 1;
+  height: 64rpx;
+  border: 2rpx solid #e2e2e2;
+  border-radius: 34rpx;
+  padding: 0 24rpx;
+  box-sizing: border-box;
+  min-width: 0;
+  margin-right: 10rpx;
+}
+
+.search-input {
+  height: 60rpx;
+  font-size: 26rpx;
+}
+
+.search-btn {
+  width: 108rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  padding: 0;
+  margin: 0;
+  font-size: 26rpx;
+  background: #52c41a;
+  color: #fff;
+  border-radius: 8rpx;
+}
+
+.search-btn::after {
+  border: none;
+}
+
+.filter-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 18rpx 16rpx;
+  flex-shrink: 0;
+}
+
+.filter-tabs {
+  display: flex;
+}
+
+.filter-tab {
+  margin-right: 40rpx;
+  font-size: 26rpx;
+  color: #111;
+  font-weight: 600;
+  position: relative;
+}
+
+.filter-tab.active {
+  color: #22a500;
+}
+
+.filter-tab.active::after {
+  content: '';
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: -6rpx;
+  height: 4rpx;
+  background: #22a500;
+}
+
+.refresh-btn {
+  height: 56rpx;
+  line-height: 56rpx;
+  padding: 0 20rpx;
+  margin: 0;
+  font-size: 24rpx;
+  background: #f5f5f5;
+  color: #333;
+  border-radius: 8rpx;
+}
+
+.refresh-btn::after {
+  border: none;
+}
+
+.content-area {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  overflow: hidden;
+}
+
+.category-list {
+  width: 202rpx;
+  height: 100%;
+  flex-shrink: 0;
+  background: #fafafa;
+}
+
+.category-item {
+  min-height: 92rpx;
+  padding: 18rpx 10rpx 18rpx 24rpx;
+  font-size: 24rpx;
+  color: #111;
+  font-weight: 600;
+}
+
+.category-item.active {
+  color: #189b00;
+  background: #fff;
+}
+
+.list-box {
+  flex: 1;
+  height: 100%;
+  min-width: 0;
+  padding: 0 18rpx 120rpx;
+  box-sizing: border-box;
+}
+
+.goods-row {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.check-box {
+  width: 36rpx;
+  height: 36rpx;
+  border: 2rpx solid #ccc;
+  border-radius: 6rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.check-box.checked {
+  background: #52c41a;
+  border-color: #52c41a;
+}
+
+.check-mark {
+  color: #fff;
+  font-size: 24rpx;
+}
+
+.goods-img {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 8rpx;
+  margin-right: 16rpx;
+  flex-shrink: 0;
+}
+
+.goods-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.goods-name {
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.4;
+  margin-bottom: 8rpx;
+}
+
+.goods-meta {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.price {
+  margin-left: 12rpx;
+  color: #e4393c;
+}
+
+.footer-bar {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  background: #fff;
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.06);
+  z-index: 10;
+}
+
+.selected-tip {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.confirm-btn {
+  width: 240rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  margin: 0;
+  padding: 0;
+  background: #52c41a;
+  color: #fff;
+  font-size: 28rpx;
+  border-radius: 8rpx;
+}
+
+.confirm-btn::after {
+  border: none;
+}
+</style>
 

+ 1 - 1
hdApp/src/admin/goods/category-ad.vue

@@ -240,7 +240,7 @@ export default {
     loadBouquetCategories() {
       categoryListInfo().then((res) => {
         const list = res.data || [];
-        this.bouquetCategories = list.filter((item) => item.flower == 0);
+        this.bouquetCategories = list;
         // 分类名称依赖列表,加载后刷新各 slot 分类类型展示文案
         this.adList = this.adList.map((item) => this.refreshSlotLinkLabels(item));
       });

+ 439 - 120
hdApp/src/admin/goods/categoryV2.vue

@@ -1,6 +1,6 @@
 <!--
   商品分类列表页(新版)
-  用途:花束/花材分类列表展示,新增与编辑跳转 category-edit 独立页面
+  用途:花束/花材分类列表展示;花束编辑跳转 category-edit,花材在本页弹窗编辑
 -->
 <template>
   <view class="page-container">
@@ -21,15 +21,23 @@
     </view>
 
     <!-- 列表头部 -->
-    <view class="list-header" v-if="currentTab !== 2">
+    <view
+      class="list-header"
+      :class="{ 'layout-item-class': currentTab === 1 }"
+      v-if="currentTab !== 2"
+    >
       <view class="col-name">分类名称</view>
-      <view class="col-num">商品数量</view>
-      <view class="col-pic">图文</view>
+      <view class="col-num" v-if="currentTab === 0">商品数量</view>
+      <view class="col-pic" v-if="currentTab === 0">图文</view>
       <view class="col-action">操作</view>
     </view>
 
     <!-- 分类列表:不用 scroll-view,避免小程序内点击事件被拦截 -->
-    <view class="list-container" v-if="currentTab !== 2">
+    <view
+      class="list-container"
+      :class="{ 'layout-item-class': currentTab === 1 }"
+      v-if="currentTab !== 2"
+    >
       <block v-if="!$util.isEmpty(filteredList)">
         <view
           class="list-item"
@@ -42,12 +50,13 @@
             <text class="cat-text">{{ item.categoryName }}</text>
           </view>
 
-          <view class="col-num item-num">
+          <view class="col-num item-num" v-if="currentTab === 0">
             <text>{{ item.goodsNum || 0 }}</text>
           </view>
 
           <view
             class="col-pic item-pic"
+            v-if="currentTab === 0"
             :data-id="item.id"
             :data-pic-text-id="item.picTextId"
             @click.stop="selectPicText"
@@ -80,20 +89,22 @@
         v-if="currentTab === 1"
         @click="shopCategorySet"
       >
-						<sprite-icon name="write" :size="28" custom-class="member-crown-icon"/>
-        <text class="icon">📝</text>商城分类设置
+        <view class="member-crown-icon">
+          <sprite-icon name="write" :size="28" />
+        </view>
+        <text class="toolbar-btn-text">花材分类设置</text>
       </button>
       <button class="toolbar-btn primary-btn" hover-class="none" :class="{'full-width': currentTab !== 1}" @click="addCategoryFn">
         <text class="icon">+</text>新增分类
       </button>
     </view>
 
-    <!-- 商城分类设置弹窗(自定义弹层,对齐 UI 设计图) -->
+    <!-- 花材分类设置弹窗(自定义弹层,对齐 UI 设计图) -->
     <view v-if="settingModal" class="setting-overlay" @touchmove.stop.prevent="preventTouchMove">
       <view class="setting-mask"></view>
       <view class="setting-dialog">
         <view class="setting-modal-head">
-          <text class="setting-modal-title">商城分类设置</text>
+          <text class="setting-modal-title">花材分类设置</text>
           <text class="setting-modal-close" @click.stop="closeSettingModal">×</text>
         </view>
 
@@ -142,25 +153,106 @@
         </view>
       </view>
     </view>
+
+    <!-- 花材分类新增/编辑弹窗(xhGhsItemClass) -->
+    <view v-if="itemClassModalShow" class="setting-overlay" @touchmove.stop.prevent="preventTouchMove">
+      <view class="setting-mask" @tap.stop="closeItemClassModal"></view>
+      <view class="setting-dialog item-class-dialog">
+        <view class="setting-modal-head">
+          <text class="setting-modal-title">{{ itemClassModalTitle }}</text>
+          <text class="setting-modal-close" @tap.stop="closeItemClassModal">×</text>
+        </view>
+
+        <scroll-view scroll-y class="item-class-form-body">
+          <view class="setting-form-row">
+            <text class="setting-form-label">分类名称</text>
+            <view class="setting-form-control">
+              <input
+                class="setting-input"
+                type="text"
+                v-model="itemClassForm.name"
+                placeholder="请输入分类名称"
+                maxlength="30"
+              />
+            </view>
+          </view>
+
+          <view class="setting-form-row">
+            <text class="setting-form-label">排序</text>
+            <view class="setting-form-control">
+              <input
+                class="setting-input"
+                type="number"
+                v-model="itemClassForm.inTurn"
+                placeholder="数值越大越靠前"
+              />
+              <text class="form-hint">数值越大,排序越靠前</text>
+            </view>
+          </view>
+
+          <view class="setting-form-row">
+            <text class="setting-form-label">打印显示</text>
+            <view class="setting-form-control setting-switch-wrap">
+              <switch
+                class="setting-switch"
+                :checked="itemClassForm.print == 1"
+                color="#52c41a"
+                @change="onItemClassPrintChange"
+              />
+              <text class="switch-text">{{ itemClassForm.print == 1 ? '是' : '否' }}</text>
+            </view>
+          </view>
+
+          <view class="setting-form-row setting-form-row-cover">
+            <text class="setting-form-label">分类封面</text>
+            <view class="setting-form-control">
+              <view class="cover-upload-box">
+                <htz-image-upload
+                  :max="1"
+                  :compress="true"
+                  v-model="itemClassCoverImgs"
+                  :headers="uploadHeaders"
+                  :action="uploadAction"
+                  @uploadSuccess="onItemClassCoverUpload"
+                  @imgDelete="onItemClassCoverDelete"
+                />
+              </view>
+              <text class="form-hint">建议上传正方形图片,用于分类展示</text>
+            </view>
+          </view>
+        </scroll-view>
+
+        <view class="setting-modal-footer">
+          <button class="setting-btn setting-btn-cancel" hover-class="none" @tap.stop="closeItemClassModal">取消</button>
+          <button class="setting-btn setting-btn-save" hover-class="none" @tap.stop="saveItemClass">保存</button>
+        </view>
+      </view>
+    </view>
   </view>
 </template>
 
 <script>
 import AppWrapperEmpty from '@/components/app-wrapper-empty';
-import { list } from '@/mixins';
+import htzImageUpload from '@/components/htz-image-upload/htz-image-upload.vue';
+import { mapGetters } from 'vuex';
 import { categoryListInfo, categoryDelB } from '@/api/goods';
+import { getAllItemClass, delClass, addClass, updateClass } from '@/api/item-class';
 import { categorySettingDetail, categorySettingSave } from '@/api/category-setting';
 
 export default {
   name: 'categoryV2',
   components: {
-    AppWrapperEmpty
+    AppWrapperEmpty,
+    htzImageUpload
   },
-  mixins: [list],
   data() {
     return {
       currentTab: 0,
       constant: this.$constant,
+      // 花束分类 xhCategory;花材分类 xhGhsItemClass,分表存储不可混用 flower 字段
+      bouquetList: [],
+      itemClassList: [],
+      listLoading: false,
       settingModal: false,
       settingSaving: false,
       settingForm: {
@@ -168,14 +260,33 @@ export default {
         itemCateName: '',
         isItemCateTop: 0,
         isItemCateShow: 0
-      }
+      },
+      itemClassModalShow: false,
+      itemClassModalTitle: '新增花材分类',
+      itemClassSaving: false,
+      itemClassForm: {
+        id: 0,
+        name: '',
+        inTurn: 100,
+        print: 1,
+        cover: ''
+      },
+      itemClassCoverImgs: [],
+      uploadHeaders: { token: '' }
     }
   },
   computed: {
+    ...mapGetters(['getLoginInfo']),
+    /** 花材封面上传地址 */
+    uploadAction() {
+      return (this.getLoginInfo && this.getLoginInfo.imgUploadApi) || '';
+    },
+    /** 当前 Tab 对应分类列表,字段统一后供模板渲染 */
     filteredList() {
-      if (!this.list.data) return [];
-      const list = this.list.data.filter(item => item.flower == this.currentTab);
-      // 启用分类在前,禁用分类在后,同组内保持接口原顺序
+      const source = this.currentTab === 1 ? this.itemClassList : this.bouquetList;
+      const list = (source || []).map((item) => (
+        this.currentTab === 1 ? this.normalizeItemClassRow(item) : this.normalizeBouquetRow(item)
+      ));
       return list.slice().sort((a, b) => {
         const aDisabled = a.status == 1 ? 0 : 1;
         const bDisabled = b.status == 1 ? 0 : 1;
@@ -184,8 +295,7 @@ export default {
     }
   },
   onPullDownRefresh() {
-    this.resetList();
-    this._list().then(() => {
+    this.loadCurrentTabList().finally(() => {
       uni.stopPullDownRefresh();
     });
   },
@@ -201,6 +311,7 @@ export default {
     if (options && options.tab !== undefined) {
       this.currentTab = parseInt(options.tab) || 0;
     }
+    this.uploadHeaders.token = uni.getStorageSync('token') || '';
     this.init();
   },
   methods: {
@@ -211,34 +322,87 @@ export default {
         });
         return;
       }
+      if (this.currentTab === index) return;
       this.currentTab = index;
+      this.loadCurrentTabList();
     },
-    async init() {
-      this._list();
+    init() {
+      this.loadCurrentTabList();
+    },
+    /** 按当前 Tab 拉取对应表数据 */
+    loadCurrentTabList() {
+      if (this.currentTab === 1) {
+        return this.loadItemClassList();
+      }
+      return this.loadBouquetList();
     },
-    _list() {
+    /** 花束分类:xhCategory */
+    loadBouquetList() {
+      this.listLoading = true;
       return categoryListInfo().then((res) => {
-        let data = { code: res.code, msg: res.msg, data: { list: res.data } };
-        this.completes(data);
+        this.bouquetList = res.code == 1 ? (res.data || []) : [];
+      }).finally(() => {
+        this.listLoading = false;
       });
     },
-    loadMore() {
-      if (!this.list.finished) {
-        this._list();
-      }
+    /** 花材分类:xhGhsItemClass */
+    loadItemClassList() {
+      this.listLoading = true;
+      return getAllItemClass().then((res) => {
+        const data = res.data || {};
+        this.itemClassList = data.list || data || [];
+      }).finally(() => {
+        this.listLoading = false;
+      });
+    },
+    /** 花束分类行字段 */
+    normalizeBouquetRow(item) {
+      return {
+        ...item,
+        sourceType: 'bouquet',
+        categoryName: item.categoryName || '',
+        img: item.img || '',
+        goodsNum: item.goodsNum || 0,
+        picTextId: item.picTextId || 0,
+        status: item.status,
+        default: item.default
+      };
+    },
+    /** 花材分类行字段映射到列表展示结构 */
+    normalizeItemClassRow(item) {
+      const cover = item.smallCover || item.cover || '';
+      return {
+        ...item,
+        sourceType: 'itemClass',
+        categoryName: item.name || '',
+        img: cover,
+        goodsNum: item.goodsNum || item.itemNum || 0,
+        picTextId: 0,
+        status: item.status === undefined || item.status === null ? 1 : item.status,
+        default: item.isDefault
+      };
     },
     /** 从 dataset 读取分类 id,兼容小程序 button 点击传参 */
     getCategoryIdFromEvent(e) {
       const dataset = (e && e.currentTarget && e.currentTarget.dataset) || {};
       return dataset.id;
     },
-    /** 跳转编辑页 */
+    /** 花束跳转编辑页;花材在本页弹窗编辑 */
     editCategory(e) {
       const categoryId = this.getCategoryIdFromEvent(e);
       if (!categoryId) {
         this.$msg('分类信息异常');
         return;
       }
+      if (this.currentTab === 1) {
+        const raw = this.itemClassList.find((item) => String(item.id) === String(categoryId));
+        if (!raw) {
+          this.$msg('分类信息异常');
+          return;
+        }
+        this.openItemClassEdit(raw);
+        return;
+      }
       uni.navigateTo({
         url: `/admin/goods/category-edit?categoryId=${categoryId}`,
         fail: () => {
@@ -247,8 +411,92 @@ export default {
       });
     },
     addCategoryFn() {
+      if (this.currentTab === 1) {
+        this.openItemClassAdd();
+        return;
+      }
       uni.navigateTo({
-        url: `/admin/goods/category-edit?flower=${this.currentTab}`
+        url: '/admin/goods/category-edit'
+      });
+    },
+    /** 打开花材分类新增弹窗 */
+    openItemClassAdd() {
+      this.itemClassForm = {
+        id: 0,
+        name: '',
+        inTurn: 100,
+        print: 1,
+        cover: ''
+      };
+      this.itemClassCoverImgs = [];
+      this.itemClassModalTitle = '新增花材分类';
+      this.itemClassModalShow = true;
+    },
+    /** 打开花材分类编辑弹窗,回填 xhGhsItemClass 字段 */
+    openItemClassEdit(item) {
+      const shortCover = item.shortCover || '';
+      this.itemClassForm = {
+        id: item.id || 0,
+        name: item.name || '',
+        inTurn: item.inTurn === undefined || item.inTurn === null ? 100 : item.inTurn,
+        print: item.print === undefined || item.print === null ? 1 : item.print,
+        cover: shortCover
+      };
+      const preview = item.bigCover || item.cover || '';
+      this.itemClassCoverImgs = preview ? [preview] : [];
+      this.itemClassModalTitle = '编辑花材分类';
+      this.itemClassModalShow = true;
+    },
+    closeItemClassModal() {
+      this.itemClassModalShow = false;
+    },
+    onItemClassPrintChange(e) {
+      this.itemClassForm.print = e.detail.value ? 1 : 0;
+    },
+    /** 花材封面上传成功,保存相对路径 shortUrl */
+    onItemClassCoverUpload(res) {
+      try {
+        const result = JSON.parse(res.data || '{}');
+        if (result.code == 1 && result.data) {
+          this.itemClassForm.cover = result.data.shortUrl || '';
+          if (result.data.bigUrl) {
+            this.itemClassCoverImgs = [result.data.bigUrl];
+          }
+          this.$msg('上传成功');
+        }
+      } catch (err) {
+        this.$msg('上传失败');
+      }
+    },
+    onItemClassCoverDelete() {
+      this.itemClassForm.cover = '';
+      this.itemClassCoverImgs = [];
+    },
+    /** 提交花材分类新增/编辑 */
+    saveItemClass() {
+      const name = (this.itemClassForm.name || '').trim();
+      if (!name) {
+        this.$msg('请填写分类名称');
+        return;
+      }
+      if (this.itemClassSaving) return;
+      this.itemClassSaving = true;
+      const payload = {
+        id: this.itemClassForm.id,
+        name,
+        inTurn: this.itemClassForm.inTurn,
+        print: this.itemClassForm.print,
+        cover: this.itemClassForm.cover || ''
+      };
+      const request = payload.id > 0 ? updateClass(payload) : addClass(payload);
+      request.then((res) => {
+        if (res.code == 1) {
+          this.$msg('保存成功');
+          this.closeItemClassModal();
+          this.loadItemClassList();
+        }
+      }).finally(() => {
+        this.itemClassSaving = false;
       });
     },
     delCategory(e) {
@@ -258,19 +506,21 @@ export default {
         return;
       }
       const that = this;
+      const isItemClass = this.currentTab === 1;
       uni.showModal({
         title: '提示',
-        content: '确认删除?',
+        content: isItemClass ? '确认删除?此分类的花材将移到默认分类' : '确认删除?',
         success: function (res) {
-          if (res.confirm) {
-            categoryDelB({ categoryId }).then((res) => {
-              if (res.code == 1) {
-                that.$msg('已删除');
-                that.resetList();
-                that._list();
-              }
-            });
-          }
+          if (!res.confirm) return;
+          const request = isItemClass
+            ? delClass({ id: categoryId })
+            : categoryDelB({ categoryId });
+          request.then((delRes) => {
+            if (delRes.code == 1) {
+              that.$msg('已删除');
+              that.loadCurrentTabList();
+            }
+          });
         }
       });
     },
@@ -336,7 +586,7 @@ export default {
       });
     },
     updateCategoryId(data) {
-      this.list.data.forEach((item) => {
+      this.bouquetList.forEach((item) => {
         if (item.id == data.categoryId) {
           item.picTextId = data.picTextId;
         }
@@ -358,15 +608,15 @@ export default {
 .tabs {
   display: flex;
   background: #fff;
-  padding: 0 20rpx;
-  border-bottom: 1rpx solid #eee;
+  padding: 0 20upx;
+  border-bottom: 1upx solid #eee;
 
   .tab-item {
     flex: 1;
     text-align: center;
-    font-size: 28rpx;
+    font-size: 28upx;
     color: #666;
-    padding: 24rpx 0;
+    padding: 24upx 0;
     position: relative;
 
     &.active {
@@ -379,10 +629,10 @@ export default {
       bottom: 0;
       left: 50%;
       transform: translateX(-50%);
-      width: 80rpx;
-      height: 4rpx;
+      width: 80upx;
+      height: 4upx;
       background: #52c41a;
-      border-radius: 4rpx;
+      border-radius: 4upx;
     }
   }
 }
@@ -391,15 +641,15 @@ export default {
 .list-header {
   display: flex;
   align-items: center;
-  padding: 24rpx 30rpx;
+  padding: 24upx 30upx;
   background: #fdfdfd;
-  margin-top: 20rpx;
-  border-bottom: 1rpx solid #f0f0f0;
-  border-radius: 12rpx 12rpx 0 0;
-  margin: 20rpx 20rpx 0 20rpx;
+  margin-top: 20upx;
+  border-bottom: 1upx solid #f0f0f0;
+  border-radius: 12upx 12upx 0 0;
+  margin: 20upx 20upx 0 20upx;
 
   view {
-    font-size: 24rpx;
+    font-size: 24upx;
     color: #333;
     font-weight: 500;
     text-align: center;
@@ -411,19 +661,33 @@ export default {
 .col-pic { flex: 2; flex-shrink: 0; }
 .col-action { flex: 2; flex-shrink: 0; }
 
+/* 花材分类无商品数量、图文列,加宽名称与操作区 */
+.layout-item-class {
+  .col-name {
+    flex: 4;
+  }
+
+  .col-action {
+    flex: 2.2;
+  }
+}
+
 /* List */
 .list-container {
   flex: 1;
   background: #fff;
-  margin: 0 20rpx 140rpx 20rpx;
-  border-radius: 0 0 12rpx 12rpx;
+  margin: 0 20upx 0 20upx;
+  border-radius: 0 0 12upx 12upx;
   overflow-y: auto;
+  /* 预留底部固定工具栏高度,避免最后一行被遮挡 */
+  padding-bottom: calc(200upx + env(safe-area-inset-bottom));
+  box-sizing: border-box;
 
   .list-item {
     display: flex;
     align-items: flex-start;
-    padding: 24rpx 30rpx;
-    border-bottom: 1rpx solid #f0f0f0;
+    padding: 24upx 30upx;
+    border-bottom: 1upx solid #f0f0f0;
 
     &:last-child {
       border-bottom: none;
@@ -468,17 +732,17 @@ export default {
       min-width: 0;
 
       .cat-img {
-        width: 60rpx;
-        height: 60rpx;
-        margin-right: 16rpx;
-        margin-top: 4rpx;
-        border-radius: 8rpx;
+        width: 60upx;
+        height: 60upx;
+        margin-right: 16upx;
+        margin-top: 4upx;
+        border-radius: 8upx;
         flex-shrink: 0;
       }
       .cat-text {
         flex: 1;
         min-width: 0;
-        font-size: 28rpx;
+        font-size: 28upx;
         color: #333;
         font-weight: 500;
         line-height: 1.5;
@@ -495,7 +759,7 @@ export default {
     }
 
     .item-num {
-      font-size: 28rpx;
+      font-size: 28upx;
       color: #3b5bdf;
       text-align: center;
     }
@@ -507,14 +771,14 @@ export default {
       .pic-tag {
         display: flex;
         align-items: center;
-        font-size: 22rpx;
-        padding: 4rpx 10rpx;
-        border-radius: 6rpx;
+        font-size: 22upx;
+        padding: 4upx 10upx;
+        border-radius: 6upx;
 
         &.no-set {
           color: #999;
           .pic-icon-box {
-            border: 2rpx solid #ccc;
+            border: 2upx solid #ccc;
             .pic-icon-inner { background: #ccc; }
           }
         }
@@ -522,23 +786,23 @@ export default {
         &.is-set {
           color: #52c41a;
           .pic-icon-box {
-            border: 2rpx solid #52c41a;
+            border: 2upx solid #52c41a;
             .pic-icon-inner { background: #52c41a; }
           }
         }
 
         .pic-icon-box {
-          width: 24rpx;
-          height: 20rpx;
-          border-radius: 4rpx;
+          width: 24upx;
+          height: 20upx;
+          border-radius: 4upx;
           display: flex;
           align-items: center;
           justify-content: center;
-          margin-right: 8rpx;
+          margin-right: 8upx;
 
           .pic-icon-inner {
-            width: 12rpx;
-            height: 10rpx;
+            width: 12upx;
+            height: 10upx;
             clip-path: polygon(0 100%, 40% 40%, 60% 70%, 80% 20%, 100% 100%);
           }
         }
@@ -549,7 +813,7 @@ export default {
       display: flex;
       justify-content: center;
       align-items: center;
-      font-size: 26rpx;
+      font-size: 26upx;
       white-space: nowrap;
 
       .action-btn {
@@ -565,7 +829,7 @@ export default {
         background: transparent;
         border: none;
         line-height: 1.4;
-        font-size: 26rpx;
+        font-size: 26upx;
         font-weight: normal;
       }
 
@@ -578,7 +842,7 @@ export default {
       }
       .action-divider {
         color: #eee;
-        margin: 0 16rpx;
+        margin: 0 16upx;
       }
       .delete-btn {
         color: #FF3B30;
@@ -594,9 +858,9 @@ export default {
   left: 0;
   right: 0;
   background: #fff;
-  padding: 20rpx 30rpx;
-  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
-  box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
+  padding: 20upx 30upx;
+  padding-bottom: calc(20upx + env(safe-area-inset-bottom));
+  box-shadow: 0 -2upx 10upx rgba(0,0,0,0.05);
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -604,27 +868,39 @@ export default {
 
   .toolbar-btn {
     flex: 1;
-    height: 80rpx;
-    border-radius: 8rpx;
+    height: 80upx;
+    border-radius: 8upx;
     display: flex;
     align-items: center;
     justify-content: center;
-    font-size: 28rpx;
+    font-size: 28upx;
     font-weight: bold;
-    margin: 0 10rpx;
+    margin: 0 10upx;
     padding: 0;
-    line-height: 80rpx;
+    line-height: 80upx;
 
     &::after {
       border: none;
     }
 
     .icon {
-      margin-right: 8rpx;
-      font-size: 32rpx;
+      margin-right: 8upx;
+      font-size: 32upx;
       font-weight: normal;
     }
 
+    .member-crown-icon {
+      margin-right: 10upx;
+      flex-shrink: 0;
+      display: flex;
+      align-items: center;
+    }
+
+    .toolbar-btn-text {
+      font-size: 28upx;
+      font-weight: bold;
+    }
+
     &.full-width {
       margin: 0;
     }
@@ -632,7 +908,7 @@ export default {
     &.outline-btn {
       background: #fff;
       color: #52c41a;
-      border: 2rpx solid #52c41a;
+      border: 2upx solid #52c41a;
     }
 
     &.primary-btn {
@@ -674,10 +950,10 @@ export default {
 .setting-dialog {
   position: relative;
   z-index: 2;
-  width: 620rpx;
+  width: 620upx;
   max-width: 86%;
   background: #fff;
-  border-radius: 16rpx;
+  border-radius: 16upx;
   overflow: hidden;
   box-sizing: border-box;
 }
@@ -687,13 +963,13 @@ export default {
   display: flex;
   align-items: center;
   justify-content: center;
-  height: 96rpx;
-  padding: 0 32rpx;
-  border-bottom: 1rpx solid #f0f0f0;
+  height: 96upx;
+  padding: 0 32upx;
+  border-bottom: 1upx solid #f0f0f0;
 }
 
 .setting-modal-title {
-  font-size: 32rpx;
+  font-size: 32upx;
   color: #333;
   font-weight: 600;
   line-height: 1.4;
@@ -701,26 +977,26 @@ export default {
 
 .setting-modal-close {
   position: absolute;
-  right: 24rpx;
+  right: 24upx;
   top: 50%;
   transform: translateY(-50%);
-  width: 44rpx;
-  height: 44rpx;
-  line-height: 44rpx;
+  width: 44upx;
+  height: 44upx;
+  line-height: 44upx;
   text-align: center;
-  font-size: 36rpx;
+  font-size: 36upx;
   color: #bbb;
 }
 
 .setting-form-body {
-  padding: 36rpx 32rpx 8rpx;
+  padding: 36upx 32upx 8upx;
 }
 
 .setting-form-row {
   display: flex;
   align-items: center;
-  min-height: 72rpx;
-  margin-bottom: 24rpx;
+  min-height: 72upx;
+  margin-bottom: 24upx;
 }
 
 .setting-form-row-last {
@@ -728,9 +1004,9 @@ export default {
 }
 
 .setting-form-label {
-  width: 196rpx;
+  width: 196upx;
   flex-shrink: 0;
-  font-size: 28rpx;
+  font-size: 28upx;
   color: #333;
   line-height: 1.4;
 }
@@ -742,12 +1018,12 @@ export default {
 
 .setting-input {
   width: 100%;
-  height: 64rpx;
-  padding: 0 20rpx;
+  height: 64upx;
+  padding: 0 20upx;
   box-sizing: border-box;
-  border: 1rpx solid #dcdfe6;
-  border-radius: 8rpx;
-  font-size: 28rpx;
+  border: 1upx solid #dcdfe6;
+  border-radius: 8upx;
+  font-size: 28upx;
   color: #333;
   background: #fff;
 }
@@ -756,7 +1032,7 @@ export default {
   display: flex;
   align-items: center;
   justify-content: flex-start;
-  min-height: 64rpx;
+  min-height: 64upx;
 }
 
 .setting-switch {
@@ -767,17 +1043,17 @@ export default {
 .setting-modal-footer {
   display: flex;
   flex-direction: row;
-  padding: 28rpx 32rpx 32rpx;
+  padding: 28upx 32upx 32upx;
 }
 
 .setting-btn {
   flex: 1;
-  height: 80rpx;
-  line-height: 80rpx;
+  height: 80upx;
+  line-height: 80upx;
   margin: 0;
   padding: 0;
-  border-radius: 8rpx;
-  font-size: 30rpx;
+  border-radius: 8upx;
+  font-size: 30upx;
   font-weight: 500;
   text-align: center;
   box-sizing: border-box;
@@ -788,20 +1064,63 @@ export default {
 }
 
 .setting-btn-cancel {
-  margin-right: 24rpx;
+  margin-right: 24upx;
   color: #52c41a;
   background: #fff;
-  border: 2rpx solid #52c41a;
+  border: 2upx solid #52c41a;
 }
 
 .setting-btn-save {
   color: #fff;
   background: #52c41a;
-  border: 2rpx solid #52c41a;
+  border: 2upx solid #52c41a;
 }
 
 .setting-btn-cancel:active,
 .setting-btn-save:active {
   opacity: 0.85;
 }
+
+.item-class-dialog {
+  max-height: 86vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.item-class-form-body {
+  flex: 1;
+  min-height: 0;
+  max-height: 58vh;
+  padding: 32upx 32upx 8upx;
+  box-sizing: border-box;
+}
+
+.setting-form-row-cover {
+  align-items: flex-start;
+}
+
+.setting-form-row-cover .setting-form-label {
+  margin-top: 8upx;
+}
+
+.form-hint {
+  display: block;
+  margin-top: 10upx;
+  font-size: 22upx;
+  color: #999;
+  line-height: 1.4;
+}
+
+.switch-text {
+  margin-left: 12upx;
+  font-size: 26upx;
+  color: #666;
+}
+
+.cover-upload-box {
+  padding: 16upx;
+  border: 2upx dashed #d9f7be;
+  border-radius: 12upx;
+  background: #fafafa;
+}
 </style>

+ 1 - 1
hdApp/src/utils/mainIndexSprite.js

@@ -17,7 +17,7 @@ const DEFAULT_SHEET_KEY = "main";
  */
 export const SPRITE_SHEETS = {
   main: {
-    path: "/hzg/sprite_icon/main_index.wbm",
+    path: "/hzg/sprite_icon/main_index.webp",
     width: 500,
     height: 500,
     icons: {