v4.0.18版本更新
This commit is contained in:
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import { Button, Popconfirm, Space, Table, Typography, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { PartnerAssocSummary, PartnerAssocUserItem } from '@dukang/shared-types';
|
||||
import { request, type HqProfile, type Paginated } from '../lib/api';
|
||||
import { request, requestDownload, type HqProfile, type Paginated } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import ActivityPosterDownloadModal from './ActivityPosterDownloadModal';
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
const [canEditAssoc, setCanEditAssoc] = useState(false);
|
||||
const [canDownloadPosters, setCanDownloadPosters] = useState(false);
|
||||
const [posterDownloadOpen, setPosterDownloadOpen] = useState(false);
|
||||
const [downloadingQr, setDownloadingQr] = useState(false);
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
const data = await request<PartnerAssocSummary>(`/admin/partners/${partnerId}/assoc`);
|
||||
@@ -78,6 +79,26 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBareQrcode() {
|
||||
if (!summary?.qrcodeUrl) {
|
||||
message.warning('尚未生成关联码');
|
||||
return;
|
||||
}
|
||||
setDownloadingQr(true);
|
||||
try {
|
||||
await requestDownload(
|
||||
`/admin/partners/${partnerId}/assoc/qrcode`,
|
||||
{},
|
||||
`partner-assoc-${partnerId}.png`,
|
||||
);
|
||||
message.success('已开始下载二维码');
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '下载二维码失败');
|
||||
} finally {
|
||||
setDownloadingQr(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function unbind(userId: string) {
|
||||
try {
|
||||
await request(`/admin/partners/${partnerId}/assoc/users/${userId}/unbind`, { method: 'POST' });
|
||||
@@ -137,12 +158,19 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
关联用户 {summary?.userCount ?? 0} 人
|
||||
</Link>
|
||||
</Typography.Paragraph>
|
||||
<Button loading={issuing} onClick={() => void reissue()}>
|
||||
{summary?.qrcodeUrl ? '补发关联码' : '生成关联码'}
|
||||
</Button>
|
||||
{canDownloadPosters ? (
|
||||
<Button onClick={() => setPosterDownloadOpen(true)}>下载活动图</Button>
|
||||
) : null}
|
||||
<Space wrap>
|
||||
<Button loading={issuing} onClick={() => void reissue()}>
|
||||
{summary?.qrcodeUrl ? '补发关联码' : '生成关联码'}
|
||||
</Button>
|
||||
{summary?.qrcodeUrl ? (
|
||||
<Button loading={downloadingQr} onClick={() => void downloadBareQrcode()}>
|
||||
下载二维码
|
||||
</Button>
|
||||
) : null}
|
||||
{canDownloadPosters ? (
|
||||
<Button onClick={() => setPosterDownloadOpen(true)}>下载活动图</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
</Space>
|
||||
<Table
|
||||
|
||||
@@ -175,3 +175,12 @@ export function storeLeafCategoryIds(store: {
|
||||
const id = String(store.categoryId || store.category?.id || '');
|
||||
return id ? [id] : [];
|
||||
}
|
||||
|
||||
/** C 端是否渲染「核销N次」;开关关闭或次数为 0 时不展示 */
|
||||
export function shouldShowStoreRedeemCount(
|
||||
enabled: boolean | undefined,
|
||||
count?: number | null,
|
||||
): boolean {
|
||||
if (enabled === false) return false;
|
||||
return Number(count) > 0;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import StoreRedeemMarquee, { type StoreRedeemMarqueeItem } from '../../component
|
||||
import BenefitIntroCard from '../../components/BenefitIntroCard';
|
||||
import WechatShareReady from '../../components/WechatShareReady';
|
||||
import { request, toast, isLoggedIn } from '../../lib/api';
|
||||
import { fetchClientConfig } from '../../lib/pay-wechat';
|
||||
import { toMoneyNumber } from '../../lib/money';
|
||||
import { maskPhone, toDialablePhone } from '../../lib/phone';
|
||||
import { track } from '../../lib/analytics';
|
||||
import {
|
||||
fullStoreAddress,
|
||||
shouldShowStoreRedeemCount,
|
||||
storeCategoryTags,
|
||||
storeStarCount,
|
||||
type StoreCategoryTreeNode,
|
||||
@@ -190,6 +192,7 @@ export default function StoreDetailPage() {
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [headerSolid, setHeaderSolid] = useState(false);
|
||||
const [pendingRatingId, setPendingRatingId] = useState<string | null>(null);
|
||||
const [showStoreRedeemCount, setShowStoreRedeemCount] = useState(true);
|
||||
const storeRef = useRef<Store | null>(null);
|
||||
storeRef.current = store;
|
||||
|
||||
@@ -197,6 +200,12 @@ export default function StoreDetailPage() {
|
||||
setHeaderSolid(scrollTop > 100);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void fetchClientConfig()
|
||||
.then((cfg) => setShowStoreRedeemCount(cfg.showStoreRedeemCount !== false))
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
const loadStore = useCallback(async (id: string) => {
|
||||
if (!id) {
|
||||
setLoading(false);
|
||||
@@ -440,7 +449,7 @@ export default function StoreDetailPage() {
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
{Number(store.redeemCount) > 0 ? (
|
||||
{shouldShowStoreRedeemCount(showStoreRedeemCount, store.redeemCount) ? (
|
||||
<Text className="store-detail-redeem">核销{store.redeemCount}次</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { FALLBACK_CITY_CODE } from '../../lib/product-images';
|
||||
import { formatDistanceMeters } from '../../lib/geo';
|
||||
import { getToken, request, toast } from '../../lib/api';
|
||||
import { fetchClientConfig } from '../../lib/pay-wechat';
|
||||
import {
|
||||
getStoresListCache,
|
||||
isStoresSessionBootstrapped,
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
toWeappShareTimeline,
|
||||
} from '../../lib/wechat-share';
|
||||
import BenefitSloganBar from '../../components/BenefitSloganBar';
|
||||
import { fullStoreAddress, storeCategoryTags, storeLeafCategoryIds, storeStarCount } from '../../lib/store-display';
|
||||
import { fullStoreAddress, shouldShowStoreRedeemCount, storeCategoryTags, storeLeafCategoryIds, storeStarCount } from '../../lib/store-display';
|
||||
import openBadgeImg from '../../assets/icons/store-open-badge.png';
|
||||
|
||||
type Store = {
|
||||
@@ -123,13 +124,21 @@ export default function StoresPage() {
|
||||
const [categoryTree, setCategoryTree] = useState<StoreCategoryNode[]>([]);
|
||||
const [sort, setSort] = useState<StoreSortKey>(() => cached?.sort ?? 'nearby');
|
||||
const [sortOpen, setSortOpen] = useState(false);
|
||||
const [showStoreRedeemCount, setShowStoreRedeemCount] = useState(true);
|
||||
const fetchCityKeyRef = useRef<string | null>(cached?.cityKey ?? null);
|
||||
const fetchSeqRef = useRef(0);
|
||||
const regionRef = useRef(region);
|
||||
regionRef.current = region;
|
||||
const regionLabel = formatRegionLabel(region);
|
||||
const categoryLabel = formatCategoryLabel(category);
|
||||
const sortLabel = STORE_SORT_OPTIONS.find((o) => o.key === sort)?.label ?? '附近优先';
|
||||
const sortOptions = useMemo(
|
||||
() =>
|
||||
showStoreRedeemCount
|
||||
? STORE_SORT_OPTIONS
|
||||
: STORE_SORT_OPTIONS.filter((o) => o.key !== 'redeem'),
|
||||
[showStoreRedeemCount],
|
||||
);
|
||||
const sortLabel = sortOptions.find((o) => o.key === sort)?.label ?? '附近优先';
|
||||
const showBootLoading = loading && stores.length === 0;
|
||||
|
||||
const childIdsByParent = useMemo(() => {
|
||||
@@ -143,6 +152,22 @@ export default function StoresPage() {
|
||||
return map;
|
||||
}, [categoryTree]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchClientConfig()
|
||||
.then((cfg) => {
|
||||
const enabled = cfg.showStoreRedeemCount !== false;
|
||||
setShowStoreRedeemCount(enabled);
|
||||
if (!enabled) {
|
||||
setSort((prev) => {
|
||||
if (prev !== 'redeem') return prev;
|
||||
patchStoresFilterCache({ sort: 'nearby' });
|
||||
return 'nearby';
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
async function fetchStores(
|
||||
nextCode: string,
|
||||
coords: UserCoords | null,
|
||||
@@ -329,7 +354,7 @@ export default function StoresPage() {
|
||||
if (sort === 'rating') {
|
||||
const diff = storeStarCount(b.rating) - storeStarCount(a.rating);
|
||||
if (diff !== 0) return diff;
|
||||
} else if (sort === 'redeem') {
|
||||
} else if (sort === 'redeem' && showStoreRedeemCount) {
|
||||
const diff = (b.redeemCount ?? 0) - (a.redeemCount ?? 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
@@ -338,7 +363,7 @@ export default function StoresPage() {
|
||||
return da - db;
|
||||
});
|
||||
return next;
|
||||
}, [stores, region, category, keyword, sort, childIdsByParent]);
|
||||
}, [stores, region, category, keyword, sort, childIdsByParent, showStoreRedeemCount]);
|
||||
|
||||
function applySearch() {
|
||||
const next = keywordInput.trim();
|
||||
@@ -497,7 +522,7 @@ export default function StoresPage() {
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
{Number(s.redeemCount) > 0 ? (
|
||||
{shouldShowStoreRedeemCount(showStoreRedeemCount, s.redeemCount) ? (
|
||||
<Text className="store-card-redeem">核销{s.redeemCount}次</Text>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -551,7 +576,7 @@ export default function StoresPage() {
|
||||
</Text>
|
||||
</View>
|
||||
<View className="region-picker-list">
|
||||
{STORE_SORT_OPTIONS.map((opt) => (
|
||||
{sortOptions.map((opt) => (
|
||||
<View
|
||||
key={opt.key}
|
||||
className={`region-picker-option${sort === opt.key ? ' selected' : ''}`}
|
||||
|
||||
Reference in New Issue
Block a user