- ProductCarousel 新增 imageFit="adaptive"(widthFix + 动态高度) - 门店套餐详情按图片真实比例自适应高度,完整显示不裁剪 - 补充 v3.4.18 文档 F 项 Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { View, Image, Swiper, SwiperItem } from '@tarojs/components';
|
import { View, Image, Swiper, SwiperItem } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
|
|
||||||
@@ -7,8 +7,12 @@ type ProductCarouselProps = {
|
|||||||
alt: string;
|
alt: string;
|
||||||
variant?: 'home' | 'detail' | 'store';
|
variant?: 'home' | 'detail' | 'store';
|
||||||
previewable?: boolean;
|
previewable?: boolean;
|
||||||
/** cover=aspectFill 裁剪铺满;contain=aspectFit 缩放完整显示(门店门头固定区) */
|
/**
|
||||||
imageFit?: 'cover' | 'contain';
|
* cover=aspectFill 裁剪铺满(固定高度,可能裁切)
|
||||||
|
* contain=aspectFit 完整显示(固定高度,可能留白)
|
||||||
|
* adaptive=widthFix 按图片真实比例自适应高度,完整显示不裁剪(门店套餐详情用)
|
||||||
|
*/
|
||||||
|
imageFit?: 'cover' | 'contain' | 'adaptive';
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 商品/门店轮播(对齐 h5 ProductCarousel 三变体) */
|
/** 商品/门店轮播(对齐 h5 ProductCarousel 三变体) */
|
||||||
@@ -21,10 +25,55 @@ export default function ProductCarousel({
|
|||||||
}: ProductCarouselProps) {
|
}: ProductCarouselProps) {
|
||||||
const slides = images.length > 0 ? images : [''];
|
const slides = images.length > 0 ? images : [''];
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
// adaptive 模式:实测容器宽度与各图渲染高度
|
||||||
|
const [wrapWidth, setWrapWidth] = useState(0);
|
||||||
|
const [imgHeights, setImgHeights] = useState<number[]>([]);
|
||||||
const prefix =
|
const prefix =
|
||||||
variant === 'store' ? 'store-detail-carousel' : variant === 'home' ? 'home-carousel' : 'detail-carousel';
|
variant === 'store' ? 'store-detail-carousel' : variant === 'home' ? 'home-carousel' : 'detail-carousel';
|
||||||
const isContain = imageFit === 'contain';
|
const isContain = imageFit === 'contain';
|
||||||
const wrapClass = `${prefix}-wrap${isContain ? ` ${prefix}-wrap--contain` : ''}`;
|
const isAdaptive = imageFit === 'adaptive';
|
||||||
|
const wrapClass = `${prefix}-wrap${isContain ? ` ${prefix}-wrap--contain` : ''}${isAdaptive ? ` ${prefix}-wrap--adaptive` : ''}`;
|
||||||
|
|
||||||
|
function getWindowWidth(): number {
|
||||||
|
try {
|
||||||
|
const info = (Taro as any).getWindowInfo?.() ?? Taro.getSystemInfoSync();
|
||||||
|
return info?.windowWidth || 375;
|
||||||
|
} catch {
|
||||||
|
return 375;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdaptive) return;
|
||||||
|
const query = Taro.createSelectorQuery();
|
||||||
|
query
|
||||||
|
.select(`.${prefix}-wrap--adaptive`)
|
||||||
|
.boundingClientRect((rect: { width?: number }) => {
|
||||||
|
if (rect && rect.width) setWrapWidth(rect.width);
|
||||||
|
else setWrapWidth(getWindowWidth());
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
// 仅挂载时测量一次容器宽度(页面宽度稳定)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isAdaptive, prefix]);
|
||||||
|
|
||||||
|
function handleImageLoad(index: number, e: any) {
|
||||||
|
const detail = e?.detail;
|
||||||
|
const naturalW = detail?.width;
|
||||||
|
const naturalH = detail?.height;
|
||||||
|
if (!naturalW || !naturalH) return;
|
||||||
|
const w = wrapWidth || getWindowWidth();
|
||||||
|
const h = Math.round((naturalH / naturalW) * w);
|
||||||
|
setImgHeights((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[index] = h;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// adaptive:随当前幻灯片高度变化;未加载时给一个 3:4 兜底高度
|
||||||
|
const adaptiveHeight = imgHeights[activeIndex] || Math.round((wrapWidth || getWindowWidth()) * 0.75);
|
||||||
|
const swiperStyle = isAdaptive ? { height: `${adaptiveHeight}px` } : undefined;
|
||||||
|
|
||||||
function previewAt(index: number) {
|
function previewAt(index: number) {
|
||||||
const urls = slides.filter(Boolean);
|
const urls = slides.filter(Boolean);
|
||||||
@@ -37,6 +86,7 @@ export default function ProductCarousel({
|
|||||||
<View className={wrapClass}>
|
<View className={wrapClass}>
|
||||||
<Swiper
|
<Swiper
|
||||||
className={prefix}
|
className={prefix}
|
||||||
|
style={swiperStyle}
|
||||||
circular={slides.length > 1}
|
circular={slides.length > 1}
|
||||||
{...(variant === 'detail' && slides.length > 1 ? { autoplay: true, interval: 3500 } : {})}
|
{...(variant === 'detail' && slides.length > 1 ? { autoplay: true, interval: 3500 } : {})}
|
||||||
onChange={(e) => setActiveIndex(e.detail.current)}
|
onChange={(e) => setActiveIndex(e.detail.current)}
|
||||||
@@ -47,8 +97,9 @@ export default function ProductCarousel({
|
|||||||
<Image
|
<Image
|
||||||
className={`${prefix}-image`}
|
className={`${prefix}-image`}
|
||||||
src={src}
|
src={src}
|
||||||
mode={isContain ? 'aspectFit' : 'aspectFill'}
|
mode={isAdaptive ? 'widthFix' : isContain ? 'aspectFit' : 'aspectFill'}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
|
onLoad={isAdaptive ? (e) => handleImageLoad(index, e) : undefined}
|
||||||
onClick={previewable ? () => previewAt(index) : undefined}
|
onClick={previewable ? () => previewAt(index) : undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export default function StorePackageDetailPage() {
|
|||||||
alt={pkg.name}
|
alt={pkg.name}
|
||||||
variant="detail"
|
variant="detail"
|
||||||
previewable
|
previewable
|
||||||
|
imageFit="adaptive"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -18,6 +18,21 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* adaptive 模式:去掉固定 1:1 比例,按图片真实比例自适应高度,完整显示不裁剪 */
|
||||||
|
.detail-carousel-wrap--adaptive {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-carousel-wrap--adaptive .detail-carousel {
|
||||||
|
height: auto;
|
||||||
|
transition: height 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-carousel-wrap--adaptive .detail-carousel-image {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-carousel {
|
.detail-carousel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
| C. 合伙人账号暂停禁止登录 | 非 `ACTIVE` → 登录/发码均拒,提示「该账号已暂停使用,请联系客服人员」 |
|
| C. 合伙人账号暂停禁止登录 | 非 `ACTIVE` → 登录/发码均拒,提示「该账号已暂停使用,请联系客服人员」 |
|
||||||
| D. 门店端 申请提现 / 筛选栏留白对齐 | `WithdrawPage` 左右内边距统一 `--space-page` |
|
| D. 门店端 申请提现 / 筛选栏留白对齐 | `WithdrawPage` 左右内边距统一 `--space-page` |
|
||||||
| E. 门店端 休息中核销 → 是否开启营业 | 核销页拦截时弹「是否开启营业?」,确认即开张后继续核销 |
|
| E. 门店端 休息中核销 → 是否开启营业 | 核销页拦截时弹「是否开启营业?」,确认即开张后继续核销 |
|
||||||
|
| F. mini-user 门店套餐图片自适应完整显示 | `ProductCarousel` 新增 `imageFit="adaptive"`(widthFix + 动态高度),门店套餐详情不再裁剪 |
|
||||||
|
|
||||||
## A. mini-user 门店座机电话脱敏(中间四位隐藏)
|
## A. mini-user 门店座机电话脱敏(中间四位隐藏)
|
||||||
|
|
||||||
@@ -84,6 +85,18 @@
|
|||||||
| `apps/h5-shop/src/pages/StatusPage.tsx` `requestToggle` / `confirmToggle` | 复用开张调用 |
|
| `apps/h5-shop/src/pages/StatusPage.tsx` `requestToggle` / `confirmToggle` | 复用开张调用 |
|
||||||
| `PUT /shop/store/status` `GET /shop/store` | 切换营业 / 读取门店(已存在) |
|
| `PUT /shop/store/status` `GET /shop/store` | 切换营业 / 读取门店(已存在) |
|
||||||
|
|
||||||
|
## F. mini-user 门店套餐图片自适应完整显示(不裁剪)
|
||||||
|
|
||||||
|
门店套餐详情(`apps/mini-user/src/pages/store-package-detail/index.tsx`)的 `ProductCarousel` 此前用 `variant="detail"` 默认 `imageFit="cover"`(= `aspectFill`),配合 `.detail-carousel-wrap` 的固定 `aspect-ratio: 1` 与 `overflow:hidden`,非正方形图片被裁切 → 用户反馈「图片没显示全」。
|
||||||
|
|
||||||
|
- 新增 `imageFit="adaptive"` 模式(仅作用于门店套餐详情):
|
||||||
|
- `Image` 改用 `mode="widthFix"`,按图片真实比例缩放、完整显示、不裁剪。
|
||||||
|
- 组件挂载时实测容器宽度(`Taro.createSelectorQuery().select('.detail-carousel-wrap--adaptive').boundingClientRect`),图片 `onLoad` 拿到自然宽高后按 `容器宽 × (自然高/自然宽)` 计算每张幻灯片渲染高度,赋给 `<Swiper>` 内联 `height`,实现轮播高度自适应(多图比例不一也能逐张适配,带 0.2s 过渡)。
|
||||||
|
- 未加载前兜底高度为 `容器宽 × 0.75`(约 3:4)。
|
||||||
|
- 样式:`product-detail.css` 增加 `.detail-carousel-wrap--adaptive`,覆盖基类固定 `aspect-ratio:1` 与 `overflow:hidden`(`aspect-ratio:auto; overflow:visible`),并令 `.detail-carousel-image` 高度为 `auto`。
|
||||||
|
- `autoplay`(3.5s)/ `1/5` 计数(feature B)在 adaptive 下保持不变。
|
||||||
|
- 仅门店套餐详情传 `imageFit="adaptive"`;门店头图(`variant="store"` 仍 `contain`)、商品详情(`cover`)不受影响。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|
||||||
- [ ] 门店详情座机显示形如 `0379-12****78`(中间四位隐藏),手机号仍 `138****8000`;拨打为明文。
|
- [ ] 门店详情座机显示形如 `0379-12****78`(中间四位隐藏),手机号仍 `138****8000`;拨打为明文。
|
||||||
@@ -91,6 +104,7 @@
|
|||||||
- [ ] 合伙人账号处于非 ACTIVE(暂停 / 停用)时:短信登录与微信登录均被拦截,提示「该账号已暂停使用,请联系客服人员」,无法进入。
|
- [ ] 合伙人账号处于非 ACTIVE(暂停 / 停用)时:短信登录与微信登录均被拦截,提示「该账号已暂停使用,请联系客服人员」,无法进入。
|
||||||
- [ ] 门店管理(结算提现)页:申请提现按钮与筛选栏左右留白与其他区块一致(统一 `--space-page`),不再贴边。
|
- [ ] 门店管理(结算提现)页:申请提现按钮与筛选栏左右留白与其他区块一致(统一 `--space-page`),不再贴边。
|
||||||
- [ ] 门店休息中时进入核销:弹「是否开启营业?」;确认开张后可继续核销;取消则维持拦截;未过审门店开张被拒时给出对应提示。
|
- [ ] 门店休息中时进入核销:弹「是否开启营业?」;确认开张后可继续核销;取消则维持拦截;未过审门店开张被拒时给出对应提示。
|
||||||
|
- [ ] 门店套餐详情图片按真实比例完整显示、不再被裁切(轮播高度随图自适应);自动轮播与 `1/5` 计数仍正常。
|
||||||
- [ ] A 仅 `maskPhone` 规则、C/D/E 仅前端样式 / 交互;均无需 Prisma 迁移(C 复用 `DISABLED`)。
|
- [ ] A 仅 `maskPhone` 规则、C/D/E 仅前端样式 / 交互;均无需 Prisma 迁移(C 复用 `DISABLED`)。
|
||||||
|
|
||||||
## HQ 开发计划
|
## HQ 开发计划
|
||||||
|
|||||||
Reference in New Issue
Block a user