feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理
订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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:30(Asia/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
|
||||
|
||||
Reference in New Issue
Block a user