- {TABS.map((tab) => {
+ {tabs.map((tab) => {
const disabled =
(tab.key === 'city' && !draft.province) ||
(tab.key === 'district' && (!draft.province || !draft.city));
@@ -137,7 +190,7 @@ export default function RegionPicker({ open, value, onClose, onConfirm }: Region
className={`region-picker-tab${activeTab === tab.key ? ' active' : ''}`}
onClick={() => onTabClick(tab.key)}
>
- {tab.label}
+ {tabLabel(tab.key, draft, tab.label)}
);
})}
diff --git a/apps/h5-user/src/components/WechatShareBootstrap.tsx b/apps/h5-user/src/components/WechatShareBootstrap.tsx
new file mode 100644
index 0000000..d49165c
--- /dev/null
+++ b/apps/h5-user/src/components/WechatShareBootstrap.tsx
@@ -0,0 +1,14 @@
+import { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+import { applyDefaultWechatShare } from '../lib/wechat-share';
+
+/** 路由变化时刷新微信右上角分享卡片 */
+export default function WechatShareBootstrap() {
+ const location = useLocation();
+
+ useEffect(() => {
+ void applyDefaultWechatShare().catch(() => {});
+ }, [location.pathname, location.search]);
+
+ return null;
+}
diff --git a/apps/h5-user/src/lib/region-data.ts b/apps/h5-user/src/lib/region-data.ts
index 55dc928..7e3959c 100644
--- a/apps/h5-user/src/lib/region-data.ts
+++ b/apps/h5-user/src/lib/region-data.ts
@@ -58,6 +58,25 @@ export function formatRegion(province: string, city: string, district: string):
return `${province} ${city} ${district}`;
}
+/** 仅展示省、市两级(门店列表等场景) */
+export function formatRegionCity(province: string, city: string): string {
+ if (!province) return '';
+ if (province === REGION_ALL) return REGION_ALL;
+ if (city === REGION_ALL) return `${province} ${REGION_ALL}`;
+ if (!city) return province;
+ return `${province} ${city}`;
+}
+
+/** 门店筛选:固定为市级,不按区县过滤 */
+export function toCityLevelRegion(selection: RegionSelection): RegionSelection {
+ const normalized = normalizeRegionSelection(selection);
+ return {
+ province: normalized.province,
+ city: normalized.city,
+ district: REGION_ALL,
+ };
+}
+
export type RegionSelection = {
province: string;
city: string;
diff --git a/apps/h5-user/src/lib/wechat-share.ts b/apps/h5-user/src/lib/wechat-share.ts
new file mode 100644
index 0000000..8ff447d
--- /dev/null
+++ b/apps/h5-user/src/lib/wechat-share.ts
@@ -0,0 +1,47 @@
+import type { WechatShareData } from '@dukang/weixin-sdk';
+import { getWechatShareLink, toAppPath } from '@dukang/weixin-sdk';
+import { isWechatEnv, weixinSdk } from './weixin';
+
+export const DEFAULT_SHARE_TITLE = '你吃饭,杜康买单';
+export const DEFAULT_SHARE_DESC = '杜康好客 · 买酒享权益,全城门店可用';
+export const WECHAT_SHARE_HINT = '请点击右上角 ··· 分享给好友';
+
+export function getDefaultShareImageUrl(): string {
+ if (typeof window === 'undefined') return toAppPath('/logo.png');
+ return new URL(toAppPath('/logo.png'), window.location.origin).href;
+}
+
+export function buildDefaultShareData(
+ overrides?: Partial
,
+): WechatShareData {
+ return {
+ title: overrides?.title ?? DEFAULT_SHARE_TITLE,
+ desc: overrides?.desc ?? DEFAULT_SHARE_DESC,
+ link: overrides?.link ?? getWechatShareLink(),
+ imgUrl: overrides?.imgUrl ?? getDefaultShareImageUrl(),
+ };
+}
+
+export async function applyDefaultWechatShare(
+ overrides?: Partial,
+): Promise {
+ if (!isWechatEnv()) return;
+ await weixinSdk.setShare(buildDefaultShareData(overrides));
+}
+
+export function handleShareButtonClick(onHint: (message: string) => void): void {
+ const showHint = (message: string) => {
+ onHint(message);
+ if (message) {
+ window.setTimeout(() => onHint(''), 2500);
+ }
+ };
+
+ if (!isWechatEnv()) {
+ showHint('请在微信内打开后分享');
+ return;
+ }
+ void applyDefaultWechatShare()
+ .then(() => showHint(WECHAT_SHARE_HINT))
+ .catch(() => showHint('分享配置失败,请刷新页面后重试'));
+}
diff --git a/apps/h5-user/src/pages/HomePage.tsx b/apps/h5-user/src/pages/HomePage.tsx
index 0905d06..7c49df4 100644
--- a/apps/h5-user/src/pages/HomePage.tsx
+++ b/apps/h5-user/src/pages/HomePage.tsx
@@ -11,6 +11,7 @@ import {
FALLBACK_CITY_CODE,
resolveUserCity,
} from '../lib/wechat-location';
+import { formatRegionCity } from '../lib/region-data';
import CouponBadge from '@dukang/shared-ui/CouponBadge';
type Product = {
@@ -44,8 +45,7 @@ export default function HomePage() {
const [products, setProducts] = useState([]);
const [cities, setCities] = useState([]);
const [cityCode, setCityCode] = useState(() => localStorage.getItem(CITY_STORAGE_KEY) || FALLBACK_CITY_CODE);
- const [locatedCityLabel, setLocatedCityLabel] = useState('');
- const [citySource, setCitySource] = useState<'auto' | 'manual'>('auto');
+ const [locationLabel, setLocationLabel] = useState('定位中...');
const [toast, setToast] = useState('');
useEffect(() => {
@@ -62,10 +62,13 @@ export default function HomePage() {
}, []);
useEffect(() => {
- if (citySource !== 'auto') return;
resolveUserCity().then((resolved) => {
- if (!resolved) return;
- setLocatedCityLabel(resolved.displayCity);
+ if (!resolved) {
+ setLocationLabel('郑州市');
+ setCityCode(FALLBACK_CITY_CODE);
+ return;
+ }
+ setLocationLabel(formatRegionCity(resolved.province, resolved.city));
if (resolved.openCity && resolved.cityCode) {
setCityCode(resolved.cityCode);
} else {
@@ -74,7 +77,7 @@ export default function HomePage() {
window.setTimeout(() => setToast(''), 2200);
}
});
- }, [citySource]);
+ }, []);
useEffect(() => {
if (!cityCode) return;
@@ -95,8 +98,6 @@ export default function HomePage() {
setTab(key);
}
- const selectedCity = cities.find((c) => c.code === cityCode);
- const headerCityLabel = locatedCityLabel || selectedCity?.name || '郑州市';
const filtered = products.filter((p) => p.aromaType === tab);
const onSale = tab === 'QINGXIANG';
@@ -105,23 +106,9 @@ export default function HomePage() {
- location_on
- {headerCityLabel}
-
+
+ location_on
+ {locationLabel}
)}
/>
diff --git a/apps/h5-user/src/pages/OrderConfirmPage.tsx b/apps/h5-user/src/pages/OrderConfirmPage.tsx
index 115b797..aa9a650 100644
--- a/apps/h5-user/src/pages/OrderConfirmPage.tsx
+++ b/apps/h5-user/src/pages/OrderConfirmPage.tsx
@@ -70,6 +70,7 @@ export default function OrderConfirmPage() {
const [addresses, setAddresses] = useState([]);
const [addressId, setAddressId] = useState(params.get('addressId') || '');
const [preview, setPreview] = useState(null);
+ const [previewLoading, setPreviewLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
@@ -104,13 +105,35 @@ export default function OrderConfirmPage() {
}, [refreshProfile]);
useEffect(() => {
- if (!productId || !addressId) return;
+ if (!productId) return;
+ let cancelled = false;
+ setPreviewLoading(true);
+ const body: { productId: string; quantity: number; addressId?: string } = {
+ productId,
+ quantity,
+ };
+ if (addressId) body.addressId = addressId;
+
request('USER_H5', '/trade/orders/preview', {
method: 'POST',
- body: JSON.stringify({ productId, quantity, addressId }),
+ body: JSON.stringify(body),
})
- .then(setPreview)
- .catch((e) => setMsg(e instanceof Error ? e.message : String(e)));
+ .then((data) => {
+ if (!cancelled) setPreview(data);
+ })
+ .catch((e) => {
+ if (!cancelled) {
+ setPreview(null);
+ setMsg(e instanceof Error ? e.message : String(e));
+ }
+ })
+ .finally(() => {
+ if (!cancelled) setPreviewLoading(false);
+ });
+
+ return () => {
+ cancelled = true;
+ };
}, [productId, quantity, addressId]);
function updateQuantity(next: number) {
@@ -331,9 +354,13 @@ export default function OrderConfirmPage() {
配送方式
- {isCross ? '物流配送' : '小飞侠配送'}
+ {!addressId
+ ? '选择地址后确认'
+ : isCross
+ ? '物流配送'
+ : '小飞侠配送'}
- {!isCross && (
+ {addressId && !isCross && (
预计24小时内送达
)}
@@ -358,10 +385,14 @@ export default function OrderConfirmPage() {
>
)}
- {!preview && productId && addressId && (
+ {previewLoading && !preview && productId && (
加载订单信息...
)}
+ {!previewLoading && !preview && productId && (
+ 无法加载商品信息
+ )}
+
{msg && {msg}
}
@@ -376,10 +407,10 @@ export default function OrderConfirmPage() {
diff --git a/apps/h5-user/src/pages/OrderDetailPage.tsx b/apps/h5-user/src/pages/OrderDetailPage.tsx
index ddb79b5..f2c2b3f 100644
--- a/apps/h5-user/src/pages/OrderDetailPage.tsx
+++ b/apps/h5-user/src/pages/OrderDetailPage.tsx
@@ -1,9 +1,11 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
+import AppToast from '../components/AppToast';
import { request } from '../lib/api';
import { buildOrderAddressSelectUrl } from '../lib/navigation';
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
+import { handleShareButtonClick } from '../lib/wechat-share';
import ContactCustomerSheet from '../components/ContactCustomerSheet';
type OrderItem = {
@@ -121,6 +123,7 @@ export default function OrderDetailPage() {
const [order, setOrder] = useState
(null);
const [showCs, setShowCs] = useState(false);
const [copyHint, setCopyHint] = useState('');
+ const [shareToast, setShareToast] = useState('');
const [confirming, setConfirming] = useState(false);
const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship';
@@ -206,7 +209,7 @@ export default function OrderDetailPage() {
杜康好客
-
+
+
diff --git a/apps/h5-user/src/pages/ProductDetailPage.tsx b/apps/h5-user/src/pages/ProductDetailPage.tsx
index 4c5038e..0ceab31 100644
--- a/apps/h5-user/src/pages/ProductDetailPage.tsx
+++ b/apps/h5-user/src/pages/ProductDetailPage.tsx
@@ -3,8 +3,10 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import AppImage from '@dukang/shared-ui/AppImage';
import ProductCarousel from '../components/ProductCarousel';
+import AppToast from '../components/AppToast';
import { request } from '../lib/api';
import { track } from '../lib/analytics';
+import { handleShareButtonClick } from '../lib/wechat-share';
import { getProductCarouselImages, getProductDetailImages } from '../lib/product-images';
import type { ProductImageSource } from '../lib/product-images';
@@ -21,6 +23,7 @@ export default function ProductDetailPage() {
const navigate = useNavigate();
const [product, setProduct] = useState
(null);
const [headerSolid, setHeaderSolid] = useState(false);
+ const [toast, setToast] = useState('');
useEffect(() => {
if (id) request('USER_H5', `/catalog/products/${id}`).then(setProduct);
@@ -66,12 +69,14 @@ export default function ProductDetailPage() {
type="button"
className="product-detail-header-btn"
aria-label="分享"
- onClick={() => {}}
+ onClick={() => handleShareButtonClick(setToast)}
>
share
+
+
diff --git a/apps/h5-user/src/pages/StoreDetailPage.tsx b/apps/h5-user/src/pages/StoreDetailPage.tsx
index ac7b9ad..6e28ccb 100644
--- a/apps/h5-user/src/pages/StoreDetailPage.tsx
+++ b/apps/h5-user/src/pages/StoreDetailPage.tsx
@@ -2,8 +2,10 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import ProductCarousel from '../components/ProductCarousel';
+import AppToast from '../components/AppToast';
import { request } from '../lib/api';
import { track } from '../lib/analytics';
+import { handleShareButtonClick } from '../lib/wechat-share';
import { STITCH_STORE_MAP, getStoreGalleryImages } from '../lib/store-images';
type StoreMedia = { url: string; mediaType?: string; sortOrder?: number };
@@ -54,6 +56,7 @@ export default function StoreDetailPage() {
const [store, setStore] = useState(null);
const [benefitBalance, setBenefitBalance] = useState(0);
const [headerSolid, setHeaderSolid] = useState(false);
+ const [toast, setToast] = useState('');
useEffect(() => {
if (id) {
@@ -114,11 +117,13 @@ export default function StoreDetailPage() {
arrow_back
门店详情
- {}}>
+ handleShareButtonClick(setToast)}>
share
+
+
diff --git a/apps/h5-user/src/pages/StoreListPage.tsx b/apps/h5-user/src/pages/StoreListPage.tsx
index 28627af..0775e33 100644
--- a/apps/h5-user/src/pages/StoreListPage.tsx
+++ b/apps/h5-user/src/pages/StoreListPage.tsx
@@ -8,8 +8,9 @@ import RegionPicker from '../components/RegionPicker';
import {
DEFAULT_REGION,
FALLBACK_CITY_REGION,
- formatRegion,
+ formatRegionCity,
REGION_ALL,
+ toCityLevelRegion,
type RegionSelection,
} from '../lib/region-data';
import { FALLBACK_CITY_CODE, resolveUserCity } from '../lib/wechat-location';
@@ -62,7 +63,7 @@ export default function StoreListPage() {
const [loading, setLoading] = useState(false);
const [categoryTab, setCategoryTab] = useState('全部');
const [keyword, setKeyword] = useState('');
- const [region, setRegion] = useState(DEFAULT_REGION);
+ const [region, setRegion] = useState(() => toCityLevelRegion(DEFAULT_REGION));
const [regionPickerOpen, setRegionPickerOpen] = useState(false);
const [filterMode, setFilterMode] = useState<'auto' | 'manual'>('auto');
const [usedFallback, setUsedFallback] = useState(false);
@@ -82,7 +83,7 @@ export default function StoreListPage() {
if (filterMode !== 'auto' || geoReady) return;
resolveUserCity().then((resolved) => {
if (resolved) {
- setRegion(resolved.region);
+ setRegion(toCityLevelRegion(resolved.region));
}
setGeoReady(true);
});
@@ -103,7 +104,7 @@ export default function StoreListPage() {
code !== FALLBACK_CITY_CODE
) {
setUsedFallback(true);
- setRegion(FALLBACK_CITY_REGION);
+ setRegion(toCityLevelRegion(FALLBACK_CITY_REGION));
return null;
}
return data;
@@ -138,11 +139,8 @@ export default function StoreListPage() {
const city = s.cityName ?? '郑州市';
if (region.province !== REGION_ALL && province !== region.province) return false;
if (region.city !== REGION_ALL && city !== region.city) return false;
- if (region.district !== REGION_ALL && s.district !== region.district) return false;
return true;
});
- } else if (region.district !== REGION_ALL) {
- list = list.filter((s) => s.district === region.district);
}
const q = keyword.trim().toLowerCase();
if (q) {
@@ -156,7 +154,7 @@ export default function StoreListPage() {
return list;
}, [stores, categoryTab, keyword, region, cityCode]);
- const regionLabel = formatRegion(region.province, region.city, region.district);
+ const regionLabel = formatRegionCity(region.province, region.city);
const emptyMessage =
filterMode === 'manual' && filtered.length === 0
? '未找到匹配门店'
@@ -271,10 +269,11 @@ export default function StoreListPage() {
setRegionPickerOpen(false)}
onConfirm={(next) => {
setFilterMode('manual');
- setRegion(next);
+ setRegion(toCityLevelRegion(next));
setRegionPickerOpen(false);
}}
/>
diff --git a/apps/h5-user/src/styles.css b/apps/h5-user/src/styles.css
index 05eec1c..7c68771 100644
--- a/apps/h5-user/src/styles.css
+++ b/apps/h5-user/src/styles.css
@@ -606,21 +606,16 @@
max-width: 46vw;
}
+.tab-main-city--readonly {
+ pointer-events: none;
+ user-select: none;
+}
+
.tab-main-city-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- max-width: 72px;
-}
-
-.tab-main-city-select {
- border: none;
- background: transparent;
- font: inherit;
- color: inherit;
- max-width: 72px;
- opacity: 0.85;
- font-size: 11px;
+ max-width: 120px;
}
.tab-main-city .material-symbols-outlined {
diff --git a/packages/weixin-sdk/src/chooseImage.ts b/packages/weixin-sdk/src/chooseImage.ts
index f360ad4..39734a5 100644
--- a/packages/weixin-sdk/src/chooseImage.ts
+++ b/packages/weixin-sdk/src/chooseImage.ts
@@ -1,5 +1,5 @@
-import { getRuntimePlatform } from './env';
-import { ensureJssdkReady, normalizeJssdkPageUrl, getJssdkSignUrl } from './jssdk';
+import { getRuntimePlatform, isIosDevice, isWechatDevTools } from './env';
+import { ensureJssdkReady, normalizeJssdkPageUrl } from './jssdk';
import type { WeixinSdkConfig } from './types';
export type ChooseWechatImageOptions = {
@@ -7,6 +7,38 @@ export type ChooseWechatImageOptions = {
sourceType?: Array<'album' | 'camera'>;
};
+function delay(ms: number): Promise {
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
+}
+
+/** 将微信 chooseImage / getLocalImgData fail 的 errMsg 转为用户可读文案 */
+export function formatChooseImageFailMessage(errMsg: string): string {
+ const msg = errMsg.trim() || '无法打开相册';
+ if (/cancel/i.test(msg)) return '';
+
+ if (/offline verifying|permission value is offline/i.test(msg)) {
+ return '微信权限验证中,请稍候再试或刷新页面后重新选择图片';
+ }
+
+ if (/invalid signature|config:fail|signature/i.test(msg)) {
+ return '微信 JSSDK 签名校验失败,请刷新页面后重试';
+ }
+
+ if (/photo.*denied|photos.*denied|相册.*权限|无法访问相册|无相册权限/i.test(msg)) {
+ return '相册权限未开启,请在 iPhone「设置 → 隐私与安全性 → 照片」中允许微信访问相册';
+ }
+
+ if (/system.*camera|camera.*not.*allowed|无法访问相机|无相机权限/i.test(msg)) {
+ return '相机权限未开启,请在 iPhone「设置 → 隐私与安全性 → 相机」中允许微信使用摄像头';
+ }
+
+ if (/permission|auth|denied|授权|拒绝/i.test(msg)) {
+ return `微信选图接口未就绪(${msg}),请刷新页面后重试`;
+ }
+
+ return msg;
+}
+
async function reportChooseImageEvent(
config: WeixinSdkConfig,
payload: {
@@ -67,7 +99,34 @@ function localIdToFile(localId: string): Promise {
reject(e instanceof Error ? e : new Error('图片解析失败'));
}
},
- fail: (err) => reject(new Error(err.errMsg || '读取图片失败')),
+ fail: (err) => {
+ const raw = err.errMsg || '读取图片失败';
+ const formatted = formatChooseImageFailMessage(raw);
+ reject(new Error(formatted || raw));
+ },
+ });
+ });
+}
+
+function invokeChooseImage(
+ count: number,
+ sourceType: Array<'album' | 'camera'>,
+): Promise {
+ return new Promise((resolve, reject) => {
+ window.wx!.chooseImage!({
+ count,
+ sizeType: ['compressed'],
+ sourceType,
+ success: (res) => resolve(res.localIds ?? []),
+ fail: (err) => {
+ const raw = err.errMsg || '无法打开相册';
+ if (/cancel/i.test(raw)) {
+ resolve([]);
+ return;
+ }
+ const formatted = formatChooseImageFailMessage(raw);
+ reject(new Error(formatted || raw));
+ },
});
});
}
@@ -84,7 +143,6 @@ export async function chooseWechatImages(
const sourceType = options.sourceType ?? ['album', 'camera'];
if (platform === 'wechat-h5') {
- const pageUrl = typeof window !== 'undefined' ? getJssdkSignUrl() : '';
const sourceTypeKey = sourceType.join(',');
try {
await ensureJssdkReady({
@@ -104,26 +162,38 @@ export async function chooseWechatImages(
throw new Error(errMsg);
}
- let localIds: string[];
- try {
- localIds = await new Promise((resolve, reject) => {
- window.wx!.chooseImage!({
- count,
- sizeType: ['compressed'],
- sourceType,
- success: (res) => resolve(res.localIds ?? []),
- fail: (err) => reject(new Error(err.errMsg || '无法打开相册')),
- });
- });
- } catch (e) {
- const errMsg = e instanceof Error ? e.message : '无法打开相册';
- void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'choose' });
- throw e;
+ if (isIosDevice() && !isWechatDevTools()) {
+ await delay(500);
}
+ let localIds: string[] = [];
+ const maxAttempts = 2;
+ let lastError: Error | null = null;
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ if (attempt > 0) {
+ await delay(800);
+ }
+ try {
+ localIds = await invokeChooseImage(count, sourceType);
+ lastError = null;
+ break;
+ } catch (e) {
+ lastError = e instanceof Error ? e : new Error('无法打开相册');
+ const raw = lastError.message;
+ const retryable = /offline verifying|权限验证中|接口未就绪/i.test(raw);
+ if (attempt < maxAttempts - 1 && retryable) continue;
+ void reportChooseImageEvent(config, {
+ status: 'fail',
+ errMsg: raw,
+ sourceType: sourceTypeKey,
+ stage: 'choose',
+ });
+ throw lastError;
+ }
+ }
+ if (lastError) throw lastError;
+
if (!localIds.length) {
- const errMsg = '未选择图片';
- void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'empty' });
return [];
}
diff --git a/packages/weixin-sdk/src/index.ts b/packages/weixin-sdk/src/index.ts
index 47c8d7c..393730f 100644
--- a/packages/weixin-sdk/src/index.ts
+++ b/packages/weixin-sdk/src/index.ts
@@ -19,8 +19,14 @@ export {
export type { WechatLocationOutcome } from './location';
export { scanQrCode } from './scan';
export { invokeWechatPay } from './pay';
-export { chooseWechatImages, canUseWechatChooseImage } from './chooseImage';
+export { chooseWechatImages, canUseWechatChooseImage, formatChooseImageFailMessage } from './chooseImage';
export type { ChooseWechatImageOptions } from './chooseImage';
+export {
+ setWechatShareData,
+ canUseWechatShare,
+ getWechatShareLink,
+} from './share';
+export type { WechatShareData } from './share';
export {
getWechatOAuthUrl,
startWechatOAuthLogin,
@@ -41,6 +47,7 @@ import { getWechatLocation, getWechatLocationDetailed } from './location';
import { scanQrCode } from './scan';
import { invokeWechatPay } from './pay';
import { chooseWechatImages } from './chooseImage';
+import { setWechatShareData } from './share';
import {
wechatLogin,
handleWechatOAuthCallback,
@@ -68,5 +75,7 @@ export function createWeixinSdk(config: WeixinSdkConfig) {
clientApp: config.clientApp,
getAccessToken: config.getAccessToken,
}),
+ setShare: (data: Parameters[1]) =>
+ setWechatShareData(config, data),
};
}
diff --git a/packages/weixin-sdk/src/share.ts b/packages/weixin-sdk/src/share.ts
new file mode 100644
index 0000000..fb8a70e
--- /dev/null
+++ b/packages/weixin-sdk/src/share.ts
@@ -0,0 +1,75 @@
+import { ensureJssdkReady, normalizeJssdkPageUrl } from './jssdk';
+import { getRuntimePlatform, isWechatBrowser } from './env';
+import type { WeixinSdkConfig } from './types';
+
+export type WechatShareData = {
+ title: string;
+ desc: string;
+ link: string;
+ imgUrl: string;
+};
+
+export function canUseWechatShare(): boolean {
+ return isWechatBrowser() || getRuntimePlatform() === 'mini';
+}
+
+function applyShareData(data: WechatShareData): Promise {
+ return new Promise((resolve, reject) => {
+ const wx = window.wx;
+ if (!wx?.updateAppMessageShareData || !wx.updateTimelineShareData) {
+ reject(new Error('当前微信版本不支持分享,请升级微信后重试'));
+ return;
+ }
+
+ let pending = 2;
+ let failed = false;
+ const done = (err?: Error) => {
+ if (err && !failed) {
+ failed = true;
+ reject(err);
+ return;
+ }
+ pending -= 1;
+ if (pending === 0 && !failed) resolve();
+ };
+
+ wx.updateAppMessageShareData({
+ title: data.title,
+ desc: data.desc,
+ link: data.link,
+ imgUrl: data.imgUrl,
+ success: () => done(),
+ fail: (res) => done(new Error(res.errMsg || '设置分享给朋友失败')),
+ });
+
+ wx.updateTimelineShareData({
+ title: data.title,
+ link: data.link,
+ imgUrl: data.imgUrl,
+ success: () => done(),
+ fail: (res) => done(new Error(res.errMsg || '设置分享到朋友圈失败')),
+ });
+ });
+}
+
+/** 配置微信内分享卡片(好友 / 朋友圈) */
+export async function setWechatShareData(
+ config: WeixinSdkConfig,
+ data: WechatShareData,
+): Promise {
+ if (!canUseWechatShare()) return;
+
+ await ensureJssdkReady({
+ apiBase: config.apiBase ?? '/api/v1',
+ clientApp: config.clientApp,
+ getAccessToken: config.getAccessToken,
+ });
+
+ await applyShareData(data);
+}
+
+/** 获取当前页分享链接(去掉 hash、OAuth 回跳参数) */
+export function getWechatShareLink(rawUrl?: string): string {
+ if (typeof window === 'undefined') return rawUrl ?? '';
+ return normalizeJssdkPageUrl(rawUrl ?? window.location.href);
+}
diff --git a/packages/weixin-sdk/src/types.ts b/packages/weixin-sdk/src/types.ts
index 5842c5a..43a2902 100644
--- a/packages/weixin-sdk/src/types.ts
+++ b/packages/weixin-sdk/src/types.ts
@@ -53,6 +53,21 @@ export type WxApi = {
success?: (res: { localData: string }) => void;
fail?: (res: { errMsg: string }) => void;
}) => void;
+ updateAppMessageShareData: (options: {
+ title: string;
+ desc: string;
+ link: string;
+ imgUrl: string;
+ success?: () => void;
+ fail?: (res: { errMsg: string }) => void;
+ }) => void;
+ updateTimelineShareData: (options: {
+ title: string;
+ link: string;
+ imgUrl: string;
+ success?: () => void;
+ fail?: (res: { errMsg: string }) => void;
+ }) => void;
};
export type MiniProgramWx = {
diff --git a/server/dukang-api/src/modules/iam/auth.service.ts b/server/dukang-api/src/modules/iam/auth.service.ts
index b1ae06d..95e5d08 100644
--- a/server/dukang-api/src/modules/iam/auth.service.ts
+++ b/server/dukang-api/src/modules/iam/auth.service.ts
@@ -1393,7 +1393,8 @@ export class AuthService {
phoneVerified,
};
const accessToken = this.jwtService.sign(payload);
- const refreshToken = this.jwtService.sign(payload, { expiresIn: '30d' });
+ const refreshExpiresIn = actorType === 'STORE' ? '7d' : '30d';
+ const refreshToken = this.jwtService.sign(payload, { expiresIn: refreshExpiresIn });
return {
accessToken,
refreshToken,