feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理

订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 14:35:39 +08:00
parent 3b669f7e38
commit 9c8d5f2cad
125 changed files with 6355 additions and 1436 deletions
+88 -41
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { View, Text, Image, ScrollView } from '@tarojs/components';
import '../../styles/store-detail.css';
import '../../styles/benefit-promo.css';
import Taro, {
useDidShow,
useLoad,
@@ -12,13 +13,18 @@ import Taro, {
import PageShell from '../../components/PageShell';
import PageNavBar from '../../components/PageNavBar';
import ProductCarousel from '../../components/ProductCarousel';
import StoreRedeemMarquee from '../../components/StoreRedeemMarquee';
import StoreRedeemMarquee, { type StoreRedeemMarqueeItem } from '../../components/StoreRedeemMarquee';
import BenefitIntroCard from '../../components/BenefitIntroCard';
import WechatShareReady from '../../components/WechatShareReady';
import { request, toast } from '../../lib/api';
import { toMoneyNumber } from '../../lib/money';
import { maskPhone, toDialablePhone } from '../../lib/phone';
import { track } from '../../lib/analytics';
import { formatShanghaiDateTime } from '../../lib/datetime';
import {
storeCategoryTags,
storeStarCount,
type StoreCategoryTreeNode,
} from '../../lib/store-display';
import {
buildSceneSharePayload,
toWeappShareMessage,
@@ -63,7 +69,16 @@ type Store = {
avgPrice?: number | null;
latitude?: number | string | null;
longitude?: number | string | null;
category?: { name: string } | null;
rating?: number | string | null;
tags?: unknown;
redeemCount?: number | null;
categoryId?: string | null;
category?: {
id?: string;
name: string;
parentId?: string | null;
parent?: { name?: string } | null;
} | null;
};
type RecentRedeem = {
@@ -104,11 +119,6 @@ function pickStoreId(raw?: string | null) {
.replace(/[^\d]/g, '');
}
/** 与历史记录一致:2026-08-03 15:14:30Asia/Shanghai */
function formatRedeemTime(input?: string | null) {
return formatShanghaiDateTime(input);
}
function formatPackagePriceYuan(price: string | number) {
const n = typeof price === 'number' ? price : Number(price);
if (!Number.isFinite(n)) return '0';
@@ -122,20 +132,29 @@ function formatRedeemAmountYuan(amount: unknown) {
return n.toFixed(2).replace(/\.?0+$/, '');
}
function formatRecentRedeemLine(row: RecentRedeem) {
function SectionTitle({
children,
className,
}: {
children: string;
className?: string;
}) {
return (
<View className={`store-detail-section-title${className ? ` ${className}` : ''}`}>
<View className="store-detail-section-title-bar" />
<Text className="store-detail-section-title-text">{children}</Text>
</View>
);
}
function toMarqueeItem(row: RecentRedeem): StoreRedeemMarqueeItem | null {
try {
// 优先用服务端拼好的 text,避免客户端时区 / Intl 差异
if (row.text?.trim()) return row.text.trim();
const label = String(row.userLabel || '用户***').trim() || '用户***';
const rawTime = String(row.createdAt || '').trim();
const time =
/^\d{4}-\d{2}-\d{2}/.test(rawTime)
? rawTime.slice(0, 19).replace('T', ' ')
: formatRedeemTime(row.createdAt);
const userLabel = String(row.userLabel || '用户***').trim() || '用户***';
const amount = formatRedeemAmountYuan(row.amount);
return `${label} ${time} 核销${amount}`;
if (!amount) return null;
return { userLabel, amount };
} catch {
return '';
return null;
}
}
@@ -160,6 +179,7 @@ export default function StoreDetailPage() {
const [storeId, setStoreId] = useState(() => pickStoreId(router.params.id));
const [store, setStore] = useState<Store | null>(null);
const [recentRedeems, setRecentRedeems] = useState<RecentRedeem[]>([]);
const [categoryTree, setCategoryTree] = useState<StoreCategoryTreeNode[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState('');
const [headerSolid, setHeaderSolid] = useState(false);
@@ -236,6 +256,12 @@ export default function StoreDetailPage() {
}
}, [router.params.id, storeId, bootstrap]);
useEffect(() => {
void request<StoreCategoryTreeNode[]>('/store-categories')
.then((tree) => setCategoryTree(Array.isArray(tree) ? tree : []))
.catch(() => setCategoryTree([]));
}, []);
// 登录态变化后回到本页:重拉详情与走马灯
useDidShow(() => {
const id = pickStoreId(storeId || router.params.id);
@@ -254,8 +280,8 @@ export default function StoreDetailPage() {
});
}, [store, storeId]);
const marqueeLines = useMemo(
() => recentRedeems.map(formatRecentRedeemLine).filter(Boolean),
const marqueeItems = useMemo(
() => recentRedeems.map(toMarqueeItem).filter((row): row is StoreRedeemMarqueeItem => !!row),
[recentRedeems],
);
@@ -369,7 +395,38 @@ export default function StoreDetailPage() {
</View>
<View className="store-detail-info-card">
<Text className="store-detail-name">{store.name}</Text>
{marqueeItems.length > 0 ? (
<View className="store-detail-marquee-wrap">
<StoreRedeemMarquee
key={marqueeItems.map((r) => `${r.userLabel}|${r.amount}`).join('|')}
items={marqueeItems}
/>
</View>
) : null}
<View className="store-detail-title-row">
<Text className="store-detail-name">{store.name}</Text>
{storeCategoryTags(store, categoryTree).map((tag) => (
<Text key={tag} className="store-detail-tag">
{tag}
</Text>
))}
</View>
<View className="store-detail-rating-row">
<View className="store-detail-stars">
{[1, 2, 3, 4, 5].map((n) => (
<Text
key={n}
className={`store-detail-star${n <= storeStarCount(store.rating) ? ' store-detail-star--on' : ''}`}
>
</Text>
))}
</View>
{Number(store.redeemCount) > 0 ? (
<Text className="store-detail-redeem">{store.redeemCount}</Text>
) : null}
</View>
<View className="store-detail-row">
<Text className="store-detail-meta store-detail-meta--flex">
@@ -404,23 +461,20 @@ export default function StoreDetailPage() {
</Text>
</View>
) : null}
{store.category?.name ? (
<View className="store-detail-tags">
<Text className="store-detail-tag">{store.category.name}</Text>
</View>
) : null}
</View>
{marqueeLines.length > 0 ? (
<View className="store-detail-marquee-wrap">
<StoreRedeemMarquee key={marqueeLines.join('|')} lines={marqueeLines} />
<BenefitIntroCard showLink accent className="store-detail-benefit-intro" />
{benefitRule ? (
<View className="store-detail-section">
<SectionTitle className="store-detail-section-title--rule">使</SectionTitle>
<Text className="store-detail-intro">{benefitRule}</Text>
</View>
) : null}
{packages.length > 0 ? (
<View className="store-detail-section store-detail-section--packages">
<Text className="store-detail-section-title"></Text>
<SectionTitle></SectionTitle>
<View className="store-detail-package-list">
{packages.map((pkg, index) => (
<View
@@ -440,16 +494,9 @@ export default function StoreDetailPage() {
</View>
) : null}
{benefitRule ? (
<View className="store-detail-section">
<Text className="store-detail-section-title store-detail-section-title--rule">使</Text>
<Text className="store-detail-intro">{benefitRule}</Text>
</View>
) : null}
{intro ? (
<View className="store-detail-section">
<Text className="store-detail-section-title"></Text>
<SectionTitle></SectionTitle>
<ScrollView className="store-detail-intro-scroll" scrollY showScrollbar>
<Text className="store-detail-intro">{intro}</Text>
</ScrollView>
@@ -458,7 +505,7 @@ export default function StoreDetailPage() {
{envPhotos.length > 0 ? (
<View className="store-detail-section">
<Text className="store-detail-section-title"></Text>
<SectionTitle></SectionTitle>
<View className="store-detail-env-grid">
{envPhotos.map((url, index) => (
<View