feat(fulfillment): 同城运费与路由修复,配送单展示商品用户地址
同城 MANUAL/ZZXFX 按小飞侠价规计费,路由查询回退仓配凭证;HQ 配送单补商品、用户和收货地址。门店核销回跳与小程序核销码一并带上。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,10 +10,11 @@ import {
|
||||
const CANVAS_ID = 'redeem-qr-canvas';
|
||||
|
||||
type RedeemQrCodeProps = {
|
||||
token: string;
|
||||
/** 二维码内容:门店 H5 落地 URL,缺省回退 token */
|
||||
payload: string;
|
||||
};
|
||||
|
||||
function drawOnWeappCanvas(token: string) {
|
||||
function drawOnWeappCanvas(payload: string) {
|
||||
const page = Taro.getCurrentInstance().page;
|
||||
const query = page ? Taro.createSelectorQuery().in(page) : Taro.createSelectorQuery();
|
||||
query
|
||||
@@ -33,33 +34,33 @@ function drawOnWeappCanvas(token: string) {
|
||||
canvas.width = layoutW * dpr;
|
||||
canvas.height = layoutH * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
drawRedeemQrOnCanvas(ctx, token, drawSize);
|
||||
drawRedeemQrOnCanvas(ctx, payload, drawSize);
|
||||
});
|
||||
}
|
||||
|
||||
export default function RedeemQrCode({ token }: RedeemQrCodeProps) {
|
||||
export default function RedeemQrCode({ payload }: RedeemQrCodeProps) {
|
||||
const [imgSrc, setImgSrc] = useState('');
|
||||
const isWeapp = process.env.TARO_ENV === 'weapp';
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
if (!payload) {
|
||||
setImgSrc('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWeapp) {
|
||||
const timer = setTimeout(() => drawOnWeappCanvas(token), 120);
|
||||
const timer = setTimeout(() => drawOnWeappCanvas(payload), 120);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void buildRedeemQrDataUrl(token).then((url) => {
|
||||
void buildRedeemQrDataUrl(payload).then((url) => {
|
||||
if (!cancelled) setImgSrc(url);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [token, isWeapp]);
|
||||
}, [payload, isWeapp]);
|
||||
|
||||
return (
|
||||
<View className="redeem-qr-box">
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
BRAND_LOGO_URL,
|
||||
BRAND_LOGO_WIDE_URL,
|
||||
CUSTOMER_SERVICE_PHONE,
|
||||
CUSTOMER_SERVICE_WECOM_URL,
|
||||
QUALIFICATION_DISCLOSURE_URL,
|
||||
type ClientRuntimeConfig,
|
||||
} from '@dukang/shared-types';
|
||||
@@ -14,6 +15,8 @@ export type BrandAssets = {
|
||||
brandLogoMarkUrl: string;
|
||||
qualificationDisclosureUrl: string;
|
||||
customerServicePhone: string;
|
||||
customerServiceWecomUrl: string;
|
||||
wecomCorpId: string;
|
||||
};
|
||||
|
||||
const FALLBACK: BrandAssets = {
|
||||
@@ -22,6 +25,8 @@ const FALLBACK: BrandAssets = {
|
||||
brandLogoMarkUrl: BRAND_LOGO_MARK_URL,
|
||||
qualificationDisclosureUrl: QUALIFICATION_DISCLOSURE_URL,
|
||||
customerServicePhone: CUSTOMER_SERVICE_PHONE,
|
||||
customerServiceWecomUrl: CUSTOMER_SERVICE_WECOM_URL,
|
||||
wecomCorpId: '',
|
||||
};
|
||||
|
||||
let cached: BrandAssets | null = null;
|
||||
@@ -35,6 +40,9 @@ function fromConfig(config: ClientRuntimeConfig | null | undefined): BrandAssets
|
||||
qualificationDisclosureUrl:
|
||||
config?.qualificationDisclosureUrl?.trim() || FALLBACK.qualificationDisclosureUrl,
|
||||
customerServicePhone: config?.customerServicePhone?.trim() || FALLBACK.customerServicePhone,
|
||||
customerServiceWecomUrl:
|
||||
config?.customerServiceWecomUrl?.trim() || FALLBACK.customerServiceWecomUrl,
|
||||
wecomCorpId: config?.wecomCorpId?.trim() || FALLBACK.wecomCorpId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ const QR_OPTIONS = {
|
||||
color: { dark: '#1f1a17', light: '#ffffff' },
|
||||
} as const;
|
||||
|
||||
/** 在 2d Canvas 上绘制二维码(小程序 / H5 通用) */
|
||||
/** 在 2d Canvas 上绘制二维码(小程序 / H5 通用;payload 为落地 URL 或纯 token) */
|
||||
export function drawRedeemQrOnCanvas(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
token: string,
|
||||
payload: string,
|
||||
sizePx = QR_SIZE,
|
||||
) {
|
||||
const qr = QRCode.create(token, { errorCorrectionLevel: 'M' });
|
||||
const qr = QRCode.create(payload, { errorCorrectionLevel: 'M' });
|
||||
const count = qr.modules.size;
|
||||
const cell = sizePx / count;
|
||||
|
||||
@@ -31,11 +31,11 @@ export function drawRedeemQrOnCanvas(
|
||||
}
|
||||
|
||||
/** H5:Data URL;失败时回退到与 h5-user 相同的外部 QR 服务 */
|
||||
export async function buildRedeemQrDataUrl(token: string): Promise<string> {
|
||||
export async function buildRedeemQrDataUrl(payload: string): Promise<string> {
|
||||
try {
|
||||
return await QRCode.toDataURL(token, QR_OPTIONS);
|
||||
return await QRCode.toDataURL(payload, QR_OPTIONS);
|
||||
} catch {
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=${QR_SIZE * 2}x${QR_SIZE * 2}&data=${encodeURIComponent(token)}`;
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=${QR_SIZE * 2}x${QR_SIZE * 2}&data=${encodeURIComponent(payload)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ export default function RedeemCodePage() {
|
||||
const router = useRouter();
|
||||
const token = decodeURIComponent(router.params.token ?? '');
|
||||
const amount = Number(router.params.amount ?? 0);
|
||||
const landingUrl = router.params.landingUrl
|
||||
? decodeURIComponent(router.params.landingUrl)
|
||||
: '';
|
||||
const qrPayload = landingUrl || token;
|
||||
|
||||
const [timerSec, setTimerSec] = useState(REDEEM_TOKEN_TTL_SECONDS);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
@@ -148,7 +152,7 @@ export default function RedeemCodePage() {
|
||||
<View className="redeem-code-panel">
|
||||
<Text className="redeem-code-head">请向收银员出示此码</Text>
|
||||
<View className="redeem-qr-wrap">
|
||||
<RedeemQrCode token={token} />
|
||||
<RedeemQrCode payload={qrPayload} />
|
||||
</View>
|
||||
<View className={`redeem-timer${timerSec > 0 && timerSec < REDEEM_TOKEN_TTL_SECONDS ? ' redeem-timer--active' : ''}`}>
|
||||
<Text className="redeem-timer-value">{formatTimer(timerSec)}</Text>
|
||||
|
||||
@@ -113,12 +113,15 @@ export default function RedeemPage() {
|
||||
const body: { amount: number; couponId?: string } = { amount: value };
|
||||
if (couponId) body.couponId = couponId;
|
||||
|
||||
const data = await request<{ token: string; amount: number }>('/redeem/tokens', {
|
||||
const data = await request<{ token: string; amount: number; landingUrl?: string }>('/redeem/tokens', {
|
||||
method: 'POST',
|
||||
data: body,
|
||||
});
|
||||
const landingQs = data.landingUrl
|
||||
? `&landingUrl=${encodeURIComponent(data.landingUrl)}`
|
||||
: '';
|
||||
Taro.navigateTo({
|
||||
url: `/pages/redeem-code/index?token=${encodeURIComponent(data.token)}&amount=${data.amount}`,
|
||||
url: `/pages/redeem-code/index?token=${encodeURIComponent(data.token)}&amount=${data.amount}${landingQs}`,
|
||||
});
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : '生成失败');
|
||||
|
||||
Reference in New Issue
Block a user