zlwu před 6 měsíci
rodič
revize
5886303b1a

+ 29 - 0
docs/miniprogram-ui-spec-sanqiang.md

@@ -170,6 +170,35 @@ $success: #189b65;
 
 ## 7. 组件规范
 
+### 7.0 商品图片与多媒体规范(强约束)
+
+> 目的:统一三端商品图片展示口径,避免运营上传图片后在不同页面“变形/撑高/被截断”。
+
+#### 7.0.1 商品主图(列表缩略图)
+
+- **推荐比例**:`1:1`(正方形)
+- **推荐尺寸**:`800×800` ~ `2000×2000` 像素(JPG/PNG/WebP)
+- **小程序展示规则**:
+  - 列表/购物车/订单/任务里的商品缩略图容器为固定方图
+  - 图片采用 **居中裁切填充**(`aspectFill`):非 1:1 的图片会被裁切到方形,主体请尽量居中
+
+#### 7.0.2 商品轮播图(详情页顶部 Banner)
+
+- **推荐比例**:`4:3`
+- **推荐尺寸**:宽度约 `750px`(例如 `750×562`)
+- **小程序展示规则**:
+  - 详情页顶部轮播区域按 **4:3** 容器展示
+  - 图片采用 **居中裁切填充**(`aspectFill`):非 4:3 的图片会被裁切,主体请尽量居中
+
+#### 7.0.3 商品详情媒体(详情图/视频列表)
+
+- **图片比例**:不强制比例(建议宽度约 `750px`)
+- **小程序展示规则(图片)**:
+  - 采用 **宽度自适应高度**(`widthFix`):图片按屏幕宽度铺满,高度随原图等比变化
+  - 建议避免在图片最顶部/最底部放置关键小字,留出适当上下留白以适配不同机型
+- **小程序展示规则(视频)**:
+  - 建议 `720p` 以内,并尽量缩短时长以利加载
+
 ### 7.1 分类卡片(客户端首页)
 
 - 每页 2 行 * 5 列

+ 6 - 3
sqxp-wx/components/client/product-detail-overlay.vue

@@ -484,7 +484,8 @@ watch(
 }
 
 .skeleton-banner {
-  height: 560rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: #e8f1ed;
 }
 
@@ -578,7 +579,8 @@ watch(
 
 .banner-swiper {
   width: 100%;
-  height: 560rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: #fff;
 
   .banner-image,
@@ -595,7 +597,8 @@ watch(
 
 .banner-empty {
   width: 100%;
-  height: 560rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: #f5f7fa;
   display: flex;
   align-items: center;

+ 4 - 2
sqxp-wx/pages/client/product/detail.vue

@@ -410,7 +410,8 @@ onMounted(() => {
 /* 对齐 sanqiang-client-product-detail-ui-demo.html:banner 区边距与圆角 */
 .banner-swiper {
   width: auto;
-  height: 440rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   margin: 24rpx 32rpx 0;
   border-radius: 28rpx;
   overflow: hidden;
@@ -426,7 +427,8 @@ onMounted(() => {
 
 .banner-empty {
   width: auto;
-  height: 440rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   margin: 24rpx 32rpx 0;
   border-radius: 28rpx;
   border: 1rpx solid var(--c-border);

+ 4 - 2
sqxp-wx/pages/client/product/view.vue

@@ -302,7 +302,8 @@ onLoad((options) => {
 
 .banner-swiper {
   width: 100%;
-  height: 560rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: #fff;
 
   .banner-image, .banner-video {
@@ -313,7 +314,8 @@ onLoad((options) => {
 
 .banner-empty {
   width: 100%;
-  height: 560rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: #f8fcfa;
   display: flex;
   align-items: center;

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

@@ -768,7 +768,7 @@ page {
   }
   .product-image {
     width: 100%;
-    height: 120rpx;
+    height: 168rpx;
     background: #ebeef5;
     display: block;
   }

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

@@ -836,7 +836,7 @@ onShow(() => {
 }
 .card .product-image {
   width: 100%;
-  height: 120rpx;
+  height: 168rpx;
   background: #ebeef5;
   display: block;
 }

+ 4 - 2
sqxp-wx/pages/supplier/product/detail.vue

@@ -392,7 +392,8 @@ page {
 
 .banner-swiper {
   width: 100%;
-  height: 440rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: linear-gradient(145deg, #f4fbf8, #dff2e9);
 
   .banner-image, .banner-video {
@@ -403,7 +404,8 @@ page {
 
 .banner-empty {
   width: 100%;
-  height: 440rpx;
+  aspect-ratio: 4 / 3;
+  height: auto;
   background: linear-gradient(145deg, #f4fbf8, #dff2e9);
   display: flex;
   align-items: center;

+ 32 - 3
sqxp-wx/utils/request.js

@@ -1,21 +1,50 @@
-import { API_BASE, TOKEN_KEY } from '@/config/api'
+import { API_BASE, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/api'
 
 let clearingAuth = false
 
 async function onHttpUnauthorized() {
   if (clearingAuth) return
   clearingAuth = true
+  // 先同步清理本地登录态,避免 store 动态导入失败时仍被识别为已登录
+  uni.removeStorageSync(TOKEN_KEY)
+  uni.removeStorageSync(REFRESH_TOKEN_KEY)
+  uni.removeStorageSync(USER_INFO_KEY)
   try {
     const { useUserStore } = await import('@/stores/user')
     useUserStore().clearLoginInfo()
   } catch (error) {
     // ignore
   }
+  const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
+  const currentRoute = pages.length ? pages[pages.length - 1].route : ''
+  if (currentRoute === 'pages/login/login') {
+    clearingAuth = false
+    return
+  }
   uni.showToast({ title: '登录已失效,请重新登录', icon: 'none' })
-  uni.reLaunch({ url: '/pages/login/login' })
+  uni.reLaunch({
+    url: '/pages/login/login',
+    fail: () => {
+      // 某些端在请求回调中 reLaunch 可能失败,使用 redirectTo 兜底
+      uni.redirectTo({ url: '/pages/login/login' })
+    },
+    complete: () => {
+      setTimeout(() => {
+        clearingAuth = false
+      }, 800)
+    }
+  })
+}
+
+function isBizUnauthorized(data) {
+  if (!data || typeof data !== 'object') return false
+  return data.code === 401
 }
 
 export function request(options) {
+  if (clearingAuth) {
+    return Promise.reject(new Error('Unauthorized'))
+  }
   const rawToken = uni.getStorageSync(TOKEN_KEY)
   const token = typeof rawToken === 'string' ? rawToken.trim() : ''
   const rawUrl = options.url || ''
@@ -33,7 +62,7 @@ export function request(options) {
         ...(options.header || {})
       },
       success: (res) => {
-        if (res.statusCode === 401) {
+        if (res.statusCode === 401 || isBizUnauthorized(res.data)) {
           onHttpUnauthorized()
           reject(new Error('Unauthorized'))
           return

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

@@ -341,7 +341,7 @@
             <span class="section-tip">支持拖拽排序,最多上传9张图片/视频</span>
           </div>
           <p class="media-spec-hint">
-            建议:宽度 750px 左右(与小程序设计稿一致),比例约 5:4(宽:高),与详情页顶部轮播区域展示一致;图片单张不超过 10MB,视频单文件不超过 100MB。
+            尺寸要求:轮播图建议按 4:3(宽:高)上传(推荐宽度约 750px,例如 750×562);小程序端详情页顶部轮播区域按 4:3 展示,非 4:3 图片会以「居中裁切填充」方式展示;图片单张不超过 10MB,视频单文件不超过 100MB。
           </p>
           <div class="banner-upload-section">
             <div v-if="bannerList.length > 0" class="banner-list">
@@ -456,7 +456,7 @@
             <span class="section-tip">支持拖拽排序,可上传图片/视频</span>
           </div>
           <p class="media-spec-hint">
-            建议:图片宽度约 750px(小程序端按宽度自适应高度);视频建议 720p 内并尽量缩短时长以利加载;图片单张不超过 10MB,视频单文件不超过 100MB。
+            尺寸要求:详情媒体图片建议宽度约 750px,小程序端以「宽度自适应高度(widthFix)」方式展示,不强制比例;建议保证主体内容居中,避免边缘关键信息被不同机型裁切/遮挡。视频建议 720p 内并尽量缩短时长以利加载;图片单张不超过 10MB,视频单文件不超过 100MB。
           </p>
           <div class="banner-upload-section">
             <div v-if="detailMediaList.length > 0" class="banner-list">