zlwu 6 ماه پیش
والد
کامیت
bcca4b97fc

+ 33 - 1
backend/app/api/mini_supplier.py

@@ -411,8 +411,13 @@ def get_inquiry_detail(
         category_name = None
         first_category_id = None
         first_category_name = None
+        first_category_image = None
+        first_category_sort_order = 0
         second_category_id = None
         second_category_name = None
+        second_category_image = None
+        second_category_sort_order = 0
+        product_sort_order = int(getattr(product, "sort_order", None) or 0) if product else 0
         unit_name = None
         if product and product.category_id:
             category = category_crud.get(db, product.category_id)
@@ -422,11 +427,18 @@ def get_inquiry_detail(
                     parent_category = category_crud.get(db, category.parent_id)
                     first_category_id = parent_category.id if parent_category else category.id
                     first_category_name = parent_category.name if parent_category else category.name
+                    first_category_image = parent_category.image_url if parent_category else category.image_url
+                    first_category_sort_order = int(parent_category.sort_order or 0) if parent_category else int(category.sort_order or 0)
                     second_category_id = category.id
                     second_category_name = category.name
+                    second_category_image = category.image_url
+                    second_category_sort_order = int(category.sort_order or 0)
                 else:
                     first_category_id = category.id
                     first_category_name = category.name
+                    first_category_image = category.image_url
+                    first_category_sort_order = int(category.sort_order or 0)
+                    second_category_sort_order = int(category.sort_order or 0)
         if product and product.unit_id:
             unit = unit_crud.get(db, product.unit_id)
             unit_name = unit.name if unit else None
@@ -440,15 +452,35 @@ def get_inquiry_detail(
             "categoryName": category_name,
             "firstCategoryId": first_category_id,
             "firstCategoryName": first_category_name,
+            "firstCategoryImage": first_category_image,
             "secondCategoryId": second_category_id,
             "secondCategoryName": second_category_name,
+            "secondCategoryImage": second_category_image,
             "unitId": product.unit_id if product else None,
             "unitName": unit_name,
             "measureValue": float(product.measure_value) if product and product.measure_value is not None else None,
             "lastPrice": latest_quote_price_map.get(item.product_id),
-            "createAt": str(item.create_at) if item.create_at else None
+            "createAt": str(item.create_at) if item.create_at else None,
+            "_firstCategorySortOrder": first_category_sort_order,
+            "_secondCategorySortOrder": second_category_sort_order,
+            "_productSortOrder": product_sort_order
         })
 
+    # 与客户端首页商品区一致:一级分类排序 -> 二级分类排序 -> 商品排序 -> 名称 -> id
+    item_details.sort(
+        key=lambda row: (
+            int(row.get("_firstCategorySortOrder") or 0),
+            int(row.get("_secondCategorySortOrder") or 0),
+            int(row.get("_productSortOrder") or 0),
+            (row.get("productName") or "").lower(),
+            row.get("productId") or "",
+        )
+    )
+    for row in item_details:
+        row.pop("_firstCategorySortOrder", None)
+        row.pop("_secondCategorySortOrder", None)
+        row.pop("_productSortOrder", None)
+
     return success_response({
         "id": inquiry.id,
         "title": inquiry.title,

+ 1 - 1
sqxp-wx/components/supplier/quotation-detail-content.vue

@@ -38,7 +38,7 @@ const firstCategoryCards = computed(() => {
             map.set(firstId, {
                 id: firstId,
                 name: item.firstCategoryName || item.categoryName || '未分类',
-                imageUrl: item.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL
+                imageUrl: item.firstCategoryImage || DEFAULT_PLACEHOLDER_IMAGE_URL
             });
         }
     });

+ 45 - 1
sqxp-wx/pages/client/cart/cart.vue

@@ -187,6 +187,17 @@ const cartListLoading = ref(false)
 const cartListHasMore = ref(false)
 const nextCartPage = ref(1)
 
+/** 一级分类:用于从 firstCategoryId 映射分类图片 */
+const rootCategoryList = ref([])
+const ROOT_CATEGORY_PAGE_SIZE = 50
+const rootCategoryImageMap = computed(() => {
+  const m = new Map()
+  for (const c of rootCategoryList.value) {
+    if (c && c.id) m.set(c.id, c.imageUrl || '')
+  }
+  return m
+})
+
 const defaultAddress = computed(() => addresses.value.find((a) => a.isDefault) || addresses.value[0] || null)
 const selectedAddressId = ref('')
 const selectedAddress = computed(
@@ -219,15 +230,18 @@ const groupedCartLines = computed(() => {
     if (last && last.key === key) {
       last.items.push(it)
     } else {
+      const mappedCategoryImage = it.firstCategoryId
+        ? rootCategoryImageMap.value.get(it.firstCategoryId) || ''
+        : ''
       groups.push({
         key,
         title: it.firstCategoryName || it.categoryName || '未分类',
         imageUrl:
+          mappedCategoryImage ||
           it.firstCategoryImage ||
           it.firstCategoryImageUrl ||
           it.categoryImage ||
           it.categoryIcon ||
-          it.imageUrl ||
           ICON_CATEGORY_FALLBACK,
         items: [it]
       })
@@ -240,6 +254,35 @@ const ICON_COORDINATE = staticAssetUrl('coordinate.svg')
 const ICON_DELETE = staticAssetUrl('delete.svg')
 const ICON_CATEGORY_FALLBACK = staticAssetUrl('product-icon.svg')
 
+async function loadAllRootCategories() {
+  rootCategoryList.value = []
+  let page = 1
+  try {
+    while (true) {
+      const res = await request({
+        url: '/mini/client/product-categories/first-level',
+        method: 'GET',
+        data: { page, pageSize: ROOT_CATEGORY_PAGE_SIZE }
+      })
+      const body = parseResponseJson(res)
+      if (body.code !== 200) break
+      const d = body.data || {}
+      const list = d.list || []
+      for (const c of list) {
+        rootCategoryList.value.push({
+          id: c.id,
+          imageUrl: c.imageUrl || ''
+        })
+      }
+      const totalPages = Number(d.totalPages) || 0
+      if (page >= totalPages) break
+      page += 1
+    }
+  } catch {
+    rootCategoryList.value = []
+  }
+}
+
 async function loadCartList(reset) {
   if (cartListLoading.value) return
   const page = reset ? 1 : nextCartPage.value
@@ -508,6 +551,7 @@ async function loadCart() {
     return
   }
   await cartStore.fetchCartSummary()
+  void loadAllRootCategories()
   await loadCartList(true)
   void loadAddresses()
   const valid = new Set(cartLines.value.map((i) => i.id))

+ 12 - 20
sqxp-wx/pages/client/home/home.vue

@@ -16,11 +16,6 @@
             </view>
             <text class="hero-region-line">{{ deliveryLineLeft }}</text>
           </view>
-          <ClientImageLoader
-            class="hero-top-logo"
-            :src="IMG_LOGO_WATERMARK"
-            mode="aspectFit"
-          />
         </view>
         <view class="search-bar">
           <input
@@ -586,8 +581,9 @@ onMounted(async () => {
 onShow(() => {
   if (!initialized.value) return
   if (!userStore.isLoggedIn || !userStore.isClient) return
-  void cartStore.fetchCart()
-  void reloadProducts()
+  void Promise.all([loadAllRootCategories(), reloadSidebar(), cartStore.fetchCart()]).finally(() => {
+    void reloadProducts()
+  })
 })
 </script>
 
@@ -611,10 +607,14 @@ page {
 .home-root {
   position: relative;
   width: 100%;
+  height: 100%;
   min-height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
   /* 底部占位:略大于结算条+tab 占用高度即可;再大只会拉长「没有更多了」与结算条之间的灰区 */
-  --home-goods-scroll-pad: calc(96rpx + constant(safe-area-inset-bottom));
-  --home-goods-scroll-pad: calc(96rpx + env(safe-area-inset-bottom, 0px));
+  --home-goods-scroll-pad: calc(160rpx + constant(safe-area-inset-bottom));
+  --home-goods-scroll-pad: calc(160rpx + env(safe-area-inset-bottom, 0px));
 }
 
 /* #ifdef H5 */
@@ -645,8 +645,9 @@ page {
 
 /* #ifndef H5 */
 .inquiry-page {
-  height: 100%;
-  max-height: 100%;
+  flex: 1;
+  height: auto;
+  max-height: none;
   min-height: 0;
   background: var(--c-bg);
   overflow: hidden;
@@ -748,15 +749,6 @@ page {
   white-space: nowrap;
 }
 
-/* 与供应商首页 header-watermark 同源素材,置于顶栏右侧底部对齐 */
-.hero-top-logo {
-  flex-shrink: 0;
-  width: 168rpx;
-  height: 168rpx;
-  opacity: 0.4;
-  pointer-events: none;
-}
-
 /* demo .search:白底圆角条 + 占位灰绿字 */
 .search-bar {
   display: flex;

+ 7 - 0
sqxp-wx/pages/client/invoice/invoice.vue

@@ -62,6 +62,7 @@
 
 <script setup>
 import { ref, onMounted } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
 import { useUserStore } from '@/stores/user'
 import { request, parseResponseJson } from '@/utils/request'
 import { API_URL } from '@/config/api'
@@ -176,6 +177,12 @@ onMounted(() => {
   }
   fetchList(true)
 })
+
+onShow(() => {
+  userStore.checkLoginStatus()
+  if (!userStore.isLoggedIn || !userStore.isClient) return
+  void fetchList(true)
+})
 </script>
 
 

+ 7 - 0
sqxp-wx/pages/client/refund/refund.vue

@@ -1,5 +1,6 @@
 <script setup>
 import { ref, onMounted } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
 import { useUserStore } from '@/stores/user'
 import { request, parseResponseJson } from '@/utils/request'
 import { API_URL } from '@/config/api'
@@ -48,6 +49,12 @@ onMounted(() => {
   }
   load()
 })
+
+onShow(() => {
+  userStore.checkLoginStatus()
+  if (!userStore.isLoggedIn || !userStore.isClient) return
+  void load()
+})
 </script>
 
 <template>

+ 15 - 2
sqxp-wx/pages/client/review/list.vue

@@ -123,7 +123,7 @@
 
 <script setup>
 import { ref, computed, onMounted } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
+import { onLoad, onShow } from '@dcloudio/uni-app'
 import { useUserStore } from '@/stores/user'
 import { fetchProductReviews, fetchLikeReview, fetchReviewStatistics } from '@/api/reviewApi'
 import { formatFenAsYuan } from '@/utils/format'
@@ -259,6 +259,13 @@ const loadStatistics = async () => {
   }
 }
 
+const refreshReviewData = async () => {
+  page.value = 1
+  noMore.value = false
+  reviewList.value = []
+  await Promise.all([loadReviews(), loadStatistics()])
+}
+
 onLoad((options) => {
   if (options.productId) {
     productId = options.productId
@@ -284,11 +291,17 @@ onMounted(async () => {
     return
   }
   try {
-    await Promise.all([loadReviews(), loadStatistics()])
+    await refreshReviewData()
   } finally {
     initialLoading.value = false
   }
 })
+
+onShow(() => {
+  userStore.checkLoginStatus()
+  if (!userStore.isLoggedIn || !userStore.isClient) return
+  void refreshReviewData()
+})
 </script>
 
 

+ 7 - 0
sqxp-wx/pages/client/settlement/settlement.vue

@@ -28,6 +28,7 @@
 
 <script setup>
 import { ref, onMounted } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
 import { useUserStore } from '@/stores/user'
 import { request, parseResponseJson } from '@/utils/request'
 import { API_URL } from '@/config/api'
@@ -80,6 +81,12 @@ onMounted(() => {
   }
   load()
 })
+
+onShow(() => {
+  userStore.checkLoginStatus()
+  if (!userStore.isLoggedIn || !userStore.isClient) return
+  void load()
+})
 </script>
 
 

+ 2 - 0
sqxp-wx/pages/deliverer/home/home.vue

@@ -707,6 +707,8 @@ page {
 }
 
 .logout-pill {
+  position: relative;
+  z-index: 100;
   min-height: 56rpx;
   padding: 0 18rpx;
   border-radius: 999rpx;

+ 7 - 0
sqxp-wx/pages/deliverer/profile/profile.vue

@@ -1,5 +1,6 @@
 <script setup>
 import { computed, onMounted, ref } from 'vue';
+import { onShow } from '@dcloudio/uni-app';
 import { useUserStore } from '@/stores/user';
 import { resolveUserAvatarUrl } from '@/utils/avatar';
 import ClientSkeleton from '@/components/client/client-skeleton.vue';
@@ -53,6 +54,12 @@ onMounted(() => {
         })();
     }
 });
+
+onShow(() => {
+    if (!userStore.isLoggedIn || !userStore.isDeliverer)
+        return;
+    void loadServicePhone();
+});
 </script>
 
 <template>

+ 7 - 0
sqxp-wx/pages/deliverer/snatch/snatch.vue

@@ -1,5 +1,6 @@
 <script setup>
 import { onMounted, ref } from 'vue';
+import { onShow } from '@dcloudio/uni-app';
 import { useUserStore } from '@/stores/user';
 import { request, parseResponseJson } from '@/utils/request';
 import PhoneDialInline from '@/components/phone-dial-inline.vue';
@@ -57,6 +58,12 @@ onMounted(() => {
     }
     fetchList();
 });
+
+onShow(() => {
+    if (!userStore.isLoggedIn || !userStore.isDeliverer)
+        return;
+    void fetchList();
+});
 </script>
 
 <template>

+ 16 - 3
sqxp-wx/pages/supplier/address/address.vue

@@ -1,12 +1,25 @@
 <script setup>
 import { onShow } from '@dcloudio/uni-app';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
 import { useUserStore } from '@/stores/user';
 import { request, parseResponseJson } from '@/utils/request';
 import PhoneDialInline from '@/components/phone-dial-inline.vue';
 import { IMG_EMPTY } from '@/utils/staticAsset';
 const userStore = useUserStore();
 const list = ref([]);
+const sortedList = computed(() => {
+    const raw = Array.isArray(list.value) ? list.value : [];
+    return raw
+        .map((it, idx) => ({ it, idx }))
+        .sort((a, b) => {
+        const ad = a.it?.isDefault ? 1 : 0;
+        const bd = b.it?.isDefault ? 1 : 0;
+        if (ad !== bd)
+            return bd - ad;
+        return a.idx - b.idx;
+    })
+        .map((x) => x.it);
+});
 const loading = ref(false);
 const loadingMore = ref(false);
 const hasMore = ref(true);
@@ -179,7 +192,7 @@ onShow(() => {
         <!-- #ifdef H5 -->
         <view class="list-scroll list-scroll--h5" @scroll.passive="onH5ListScroll">
           <view class="list-inner">
-            <view v-for="item in list" :key="item.id" class="card" hover-class="card--pressed" :hover-stay-time="100">
+            <view v-for="item in sortedList" :key="item.id" class="card" hover-class="card--pressed" :hover-stay-time="100">
               <view class="card-main" @click="goEdit(item)">
                 <view class="card-top">
                   <text class="name">{{ item.contactPerson }}</text>
@@ -213,7 +226,7 @@ onShow(() => {
           @scrolltolower="onScrollToLower"
         >
           <view class="list-inner">
-            <view v-for="item in list" :key="item.id" class="card" hover-class="card--pressed" :hover-stay-time="100">
+            <view v-for="item in sortedList" :key="item.id" class="card" hover-class="card--pressed" :hover-stay-time="100">
               <view class="card-main" @click="goEdit(item)">
                 <view class="card-top">
                   <text class="name">{{ item.contactPerson }}</text>

+ 2 - 2
sqxp-wx/pages/supplier/delivery/delivery.vue

@@ -641,10 +641,10 @@ page {
     min-width: 0;
     display: flex;
     align-items: center;
-    gap: 10rpx;
+    gap: 10px;
   }
   .no-line .no {
-    flex: 1;
+    flex: 0 1 auto;
     min-width: 0;
     overflow: hidden;
     text-overflow: ellipsis;

+ 2 - 2
sqxp-wx/pages/supplier/delivery/detail/detail.vue

@@ -561,10 +561,10 @@ onLoad((query) => {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  gap: 12rpx;
+  gap: 10px;
 }
 .hero-title-row .hero-title {
-  flex: 1;
+  flex: 0 1 auto;
   min-width: 0;
   margin-bottom: 0;
   overflow: hidden;

+ 4 - 2
sqxp-wx/pages/supplier/home/home.vue

@@ -566,6 +566,8 @@ onShow(() => {
 }
 
 .logout-pill {
+  position: relative;
+  z-index: 100;
   min-height: 56rpx;
   padding: 0 18rpx;
   border-radius: 999rpx;
@@ -722,10 +724,10 @@ onShow(() => {
   min-width: 0;
   display: flex;
   align-items: center;
-  gap: 10rpx;
+  gap: 10px;
 }
 .card .no-line .no {
-  flex: 1;
+  flex: 0 1 auto;
   min-width: 0;
   overflow: hidden;
   text-overflow: ellipsis;

+ 1 - 1
sqxp-wx/pages/supplier/inquiry/inquiry.vue

@@ -44,7 +44,7 @@ const firstCategoryCards = computed(() => {
             map.set(firstId, {
                 id: firstId,
                 name: item.firstCategoryName || item.categoryName || '未分类',
-                imageUrl: item.imageUrl || DEFAULT_PLACEHOLDER_IMAGE_URL
+                imageUrl: item.firstCategoryImage || DEFAULT_PLACEHOLDER_IMAGE_URL
             });
         }
     });

+ 7 - 0
sqxp-wx/pages/supplier/profile/profile.vue

@@ -1,5 +1,6 @@
 <script setup>
 import { computed, onMounted, ref } from 'vue';
+import { onShow } from '@dcloudio/uni-app';
 import { useUserStore } from '@/stores/user';
 import { resolveUserAvatarUrl } from '@/utils/avatar';
 import ClientSkeleton from '@/components/client/client-skeleton.vue';
@@ -67,6 +68,12 @@ onMounted(() => {
         }
     })();
 });
+
+onShow(() => {
+    if (!userStore.isLoggedIn || !userStore.isSupplier)
+        return;
+    void loadServicePhone();
+});
 </script>
 
 <template>

+ 17 - 2
sqxp-wx/pages/supplier/quotation/quotation.vue

@@ -220,9 +220,12 @@ onShow(() => {
         @scrolltolower="onScrollToLower"
       >
       <view class="list-inner">
-        <view v-for="row in filteredList" :key="row.id" class="card" @click="goDetail(row.id)">
+        <view v-for="(row, idx) in filteredList" :key="row.id" class="card" @click="goDetail(row.id)">
           <view class="row top-row">
-            <text class="time">报价日期:{{ row.submitTime || '-' }}</text>
+            <view class="top-left">
+              <text class="seq">{{ idx + 1 }}.</text>
+              <text class="time">报价日期:{{ row.submitTime || '-' }}</text>
+            </view>
             <text class="st" :class="row.status === 'selected' ? 'selected' : 'submitted'">
               {{ statusText[row.status || ''] || row.status || '-' }}
             </text>
@@ -368,6 +371,18 @@ page {
     align-items: center;
     margin-bottom: 12rpx;
   }
+  .top-left {
+    display: inline-flex;
+    align-items: center;
+    gap: 10rpx;
+    min-width: 0;
+  }
+  .seq {
+    flex-shrink: 0;
+    font-size: 24rpx;
+    color: #303133;
+    font-weight: 700;
+  }
   .row {
     display: flex;
     justify-content: space-between;

+ 1 - 1
sqxp-wx/store/index.js

@@ -128,7 +128,7 @@ const store = createStore({
 		}, univerifyInfo) {
 			return new Promise((resolve, reject) => {
 				uni.request({
-					url: 'https://97fca9f2-41f6-449f-a35e-3f135d4c3875.bspapp.com/http/univerify-login',
+					url: `https://97fca9f2-41f6-449f-a35e-3f135d4c3875.bspapp.com/http/univerify-login?_t=${Date.now()}`,
 					method: 'POST',
 					data: univerifyInfo,
 					success: (res) => {

+ 1 - 1
sqxp-wx/utils/loginRsa.js

@@ -2,7 +2,7 @@ import JSEncrypt from 'jsencrypt'
 import { API_BASE } from '@/config/api'
 
 function fetchLoginPublicKey() {
-  const url = `${API_BASE}/mini/auth/login-rsa-public-key`
+  const url = `${API_BASE}/mini/auth/login-rsa-public-key?_t=${Date.now()}`
   return new Promise((resolve, reject) => {
     uni.request({
       url,

+ 13 - 1
sqxp-wx/utils/request.js

@@ -2,6 +2,17 @@ import { API_BASE, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/
 
 let clearingAuth = false
 
+function appendTimestampQuery(url) {
+  const base = String(url || '')
+  const ts = Date.now()
+  if (!base) return `${base}?_t=${ts}`
+  // 已有 _t 时覆盖为最新值,避免 query 反复堆叠
+  if (/[?&]_t=/.test(base)) {
+    return base.replace(/([?&]_t=)\d+/, `$1${ts}`)
+  }
+  return `${base}${base.includes('?') ? '&' : '?'}_t=${ts}`
+}
+
 function createUnauthorizedError(rawUrl = '') {
   const loginApiPattern = /\/mini\/auth\/login(?:$|\?)/
   const message = loginApiPattern.test(String(rawUrl))
@@ -58,9 +69,10 @@ export function request(options) {
   const rawToken = uni.getStorageSync(TOKEN_KEY)
   const token = typeof rawToken === 'string' ? rawToken.trim() : ''
   const rawUrl = options.url || ''
-  const url = rawUrl.startsWith('http')
+  const normalizedUrl = rawUrl.startsWith('http')
     ? rawUrl
     : `${API_BASE}${rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`}`
+  const url = appendTimestampQuery(normalizedUrl)
 
   return new Promise((resolve, reject) => {
     uni.request({

+ 43 - 4
web-admin/src/views/system/inquiry-quotation/index.vue

@@ -102,6 +102,7 @@
               <span class="title-bar-marker" aria-hidden="true"></span>
               <span>报价管理</span>
               <span v-if="selectedInquiry" class="subtitle">(当前询价:{{ selectedInquiry.title }})</span>
+              <span v-if="selectedInquiry" class="quote-manage-warning">发布询价后不会自动更新客户端可售商品,需在本区域确认供应商并生成生效价格。</span>
             </div>
           </div>
         </template>
@@ -1815,10 +1816,27 @@ const submitInquiryForm = async () => {
 
 const handlePublish = async (row: any) => {
   try {
+    await ElMessageBox.confirm(
+      '<span style="color:#dc2626;font-weight:600;line-height:1.75;display:inline-block;">发布后仅代表询价单进入可报价状态,客户端可售商品不会自动切换。<br/>请在【报价管理 -> 最新报价对比】中重新确认供应商并生成新的生效价格清单,客户端才会更新到本次询价商品。</span>',
+      '确认发布询价单',
+      {
+        confirmButtonText: '确认发布',
+        cancelButtonText: '取消',
+        type: 'warning',
+        dangerouslyUseHTMLString: true
+      }
+    )
     await fetchPublishInquiry(row.id)
-    ElMessage.success('发布成功')
+    ElMessage({
+      type: 'warning',
+      duration: 9000,
+      dangerouslyUseHTMLString: true,
+      message:
+        '<span style="color:#dc2626;font-weight:600;">发布成功。请立即前往【最新报价对比】重新确认供应商并生成生效价格清单,否则客户端可售商品仍沿用旧清单。</span>'
+    })
     loadInquiries()
   } catch (e: any) {
+    if (e === 'cancel' || e === 'close') return
     // 后端会返回“该区域已存在发布中的询价单...”
     ElMessage.error(e?.message || '发布失败')
   }
@@ -2449,9 +2467,24 @@ const openMatrixProductView = (row: any) => {
 
 const matrixSpanMethod = ({ row, columnIndex }: { row: any; columnIndex: number }) => {
   if (!row?.__summary) return { rowspan: 1, colspan: 1 }
-  // 汇总行:合并前 5 列(图片/商品/分类/计量/单位)
-  if (columnIndex === 0) return { rowspan: 1, colspan: 5 }
-  if (columnIndex > 0 && columnIndex < 5) return { rowspan: 0, colspan: 0 }
+  // 汇总行:合并前 4 列(商品/分类/计量/单位)
+  if (columnIndex === 0) return { rowspan: 1, colspan: 4 }
+  if (columnIndex > 0 && columnIndex < 4) return { rowspan: 0, colspan: 0 }
+
+  // 若存在“已选中供应商”列,则把汇总行的「总/均」扩展到“报价+调价系数”两列
+  const selectedSupplierId = selectedQuotationDetail.value?.supplierId
+  if (selectedSupplierId) {
+    let supplierColumnStart = 4
+    for (const s of compareSuppliers.value || []) {
+      if (s.supplierId === selectedSupplierId) {
+        if (columnIndex === supplierColumnStart) return { rowspan: 1, colspan: 2 }
+        if (columnIndex === supplierColumnStart + 1) return { rowspan: 0, colspan: 0 }
+        break
+      }
+      // 非选中供应商在矩阵中占 1 列;选中供应商占 3 列(报价/调价系数/最终价格)
+      supplierColumnStart += 1
+    }
+  }
   return { rowspan: 1, colspan: 1 }
 }
 
@@ -2912,6 +2945,12 @@ onMounted(async () => {
   color: var(--el-color-primary);
   font-weight: 400;
 }
+.quote-manage-warning {
+  margin-left: 8px;
+  color: #dc2626;
+  font-size: 12px;
+  font-weight: 600;
+}
 .actions {
   display: flex;
   align-items: center;

+ 12 - 2
web-admin/src/views/system/inquiry/index.vue

@@ -232,7 +232,7 @@
 
 <script setup lang="ts">
   import { ref, reactive, onMounted, watch } from 'vue'
-  import { ElMessage } from 'element-plus'
+  import { ElMessage, ElMessageBox } from 'element-plus'
   import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
   import { Plus, Setting } from '@element-plus/icons-vue'
   import { fetchInquiryList, fetchInquiryDetail, fetchCreateInquiry, fetchPublishInquiry, fetchCloseInquiry } from '@/api/business'
@@ -448,10 +448,20 @@
 
   const handlePublish = async (row: any) => {
     try {
+      await ElMessageBox.confirm(
+        '发布后供应商可开始报价,但客户可售商品不会立即更新。需要后续在【询价报价管理】中确认供应商并生成生效价格,客户端才会切换到新商品。',
+        '确认发布询价单',
+        {
+          confirmButtonText: '确认发布',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }
+      )
       await fetchPublishInquiry(row.id)
-      ElMessage.success('发布成功')
+      ElMessage.success('发布成功,请继续到【询价报价管理】确认供应商并生成生效价格')
       loadData()
     } catch (error) {
+      if (error === 'cancel' || error === 'close') return
       console.error('发布失败:', error)
     }
   }