v4.0.18版本更新

This commit is contained in:
2026-09-08 13:47:42 +08:00
parent 23ba639e9b
commit 253430b291
14 changed files with 136 additions and 25 deletions
@@ -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
+9
View File
@@ -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>
+31 -6
View File
@@ -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' : ''}`}