Update project documentation and enhance user interaction features

- Added a new entry for user interaction habit analysis based on agent transcripts, summarizing key insights into communication styles and preferences.
- Updated project indices to reflect the latest developments, including the addition of a wallet balance feature and enhancements to the mini program's user interface for better user experience.
- Improved the handling of loading states in the chapters page, ensuring a smoother user experience during data retrieval.
- Implemented a gift payment sharing feature, allowing users to share payment requests with friends for collaborative purchases.
This commit is contained in:
Alex-larget
2026-03-17 11:44:36 +08:00
parent b971420090
commit 0d12ab1d07
65 changed files with 3836 additions and 180 deletions

View File

@@ -104,6 +104,7 @@ export function UserDetailModal({
const [user, setUser] = useState<UserDetail | null>(null)
const [tracks, setTracks] = useState<UserTrack[]>([])
const [referrals, setReferrals] = useState<unknown[]>([])
const [balanceData, setBalanceData] = useState<{ balance: number; transactions: Array<{ id: string; type: string; amount: number; orderId?: string; createdAt: string }> } | null>(null)
const [loading, setLoading] = useState(false)
const [syncing, setSyncing] = useState(false)
const [saving, setSaving] = useState(false)
@@ -121,7 +122,6 @@ export function UserDetailModal({
// 设成超级个体VIP
const [vipForm, setVipForm] = useState({ isVip: false, vipExpireDate: '', vipRole: '', vipName: '', vipProject: '', vipContact: '', vipBio: '' })
const [vipRoles, setVipRoles] = useState<{ id: number; name: string }[]>([])
const [vipSaving, setVipSaving] = useState(false)
// 用户资料完善(神射手)
const [sssLoading, setSssLoading] = useState(false)
@@ -194,6 +194,13 @@ export function UserDetailModal({
)
if (refData?.success && refData.referrals) setReferrals(refData.referrals)
} catch { setReferrals([]) }
try {
const balData = await get<{ success?: boolean; data?: { balance: number; transactions: Array<{ id: string; type: string; amount: number; orderId?: string; createdAt: string }> } }>(
`/api/admin/users/${encodeURIComponent(userId)}/balance`,
)
if (balData?.success && balData.data) setBalanceData(balData.data)
else setBalanceData(null)
} catch { setBalanceData(null) }
} catch (e) {
console.error('Load user detail error:', e)
} finally {
@@ -222,6 +229,10 @@ export function UserDetailModal({
async function handleSave() {
if (!user) return
if (vipForm.isVip && !vipForm.vipExpireDate.trim()) {
toast.error('开启 VIP 请填写有效到期日')
return
}
setSaving(true)
try {
const payload: Record<string, unknown> = {
@@ -229,6 +240,14 @@ export function UserDetailModal({
phone: editPhone || undefined,
nickname: editNickname || undefined,
tags: JSON.stringify(editTags),
// 超级个体/VIP 相关字段一并保存
isVip: vipForm.isVip,
vipExpireDate: vipForm.isVip ? vipForm.vipExpireDate : undefined,
vipRole: vipForm.vipRole || undefined,
vipName: vipForm.vipName || undefined,
vipProject: vipForm.vipProject || undefined,
vipContact: vipForm.vipContact || undefined,
vipBio: vipForm.vipBio || undefined,
}
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload)
if (data?.success) {
@@ -268,27 +287,6 @@ export function UserDetailModal({
} catch { toast.error('修改失败') } finally { setPasswordSaving(false) }
}
async function handleSaveVip() {
if (!user) return
if (vipForm.isVip && !vipForm.vipExpireDate.trim()) { toast.error('开启 VIP 请填写有效到期日'); return }
setVipSaving(true)
try {
const payload = {
id: user.id,
isVip: vipForm.isVip,
vipExpireDate: vipForm.isVip ? vipForm.vipExpireDate : undefined,
vipRole: vipForm.vipRole || undefined,
vipName: vipForm.vipName || undefined,
vipProject: vipForm.vipProject || undefined,
vipContact: vipForm.vipContact || undefined,
vipBio: vipForm.vipBio || undefined,
}
const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload)
if (data?.success) { toast.success('VIP 设置已保存'); loadUserDetail(); onUserUpdated?.() }
else toast.error('保存失败: ' + (data?.error || ''))
} catch { toast.error('保存失败') } finally { setVipSaving(false) }
}
// 用户资料完善查询(支持多维度)
async function handleSSSQuery() {
if (!sssQueryPhone && !sssQueryOpenId && !sssQueryWechatId) {
@@ -512,7 +510,7 @@ export function UserDetailModal({
</div>
)}
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="p-4 bg-[#0a1628] rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-white">{user.referralCount ?? 0}</p>
@@ -523,6 +521,12 @@ export function UserDetailModal({
¥{(user.pendingEarnings ?? 0).toFixed(2)}
</p>
</div>
<div className="p-4 bg-[#0a1628] rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-2xl font-bold text-[#38bdac]">
¥{(balanceData?.balance ?? 0).toFixed(2)}
</p>
</div>
<div className="p-4 bg-[#0a1628] rounded-lg">
<p className="text-gray-400 text-sm"></p>
<p className="text-sm text-white">
@@ -609,14 +613,33 @@ export function UserDetailModal({
onChange={(e) => setVipForm((f) => ({ ...f, vipName: e.target.value }))}
/>
</div>
<Button
size="sm"
onClick={handleSaveVip}
disabled={vipSaving}
className="bg-amber-500/20 hover:bg-amber-500/30 text-amber-400 border border-amber-500/40"
>
{vipSaving ? '保存中...' : '保存 VIP'}
</Button>
<div className="space-y-1">
<Label className="text-gray-400 text-xs"></Label>
<Input
className="bg-[#162840] border-gray-700 text-white text-sm"
placeholder="如:某某科技"
value={vipForm.vipProject}
onChange={(e) => setVipForm((f) => ({ ...f, vipProject: e.target.value }))}
/>
</div>
<div className="space-y-1">
<Label className="text-gray-400 text-xs"></Label>
<Input
className="bg-[#162840] border-gray-700 text-white text-sm"
placeholder="微信/手机等"
value={vipForm.vipContact}
onChange={(e) => setVipForm((f) => ({ ...f, vipContact: e.target.value }))}
/>
</div>
<div className="space-y-1">
<Label className="text-gray-400 text-xs"></Label>
<Input
className="bg-[#162840] border-gray-700 text-white text-sm"
placeholder="简短介绍"
value={vipForm.vipBio}
onChange={(e) => setVipForm((f) => ({ ...f, vipBio: e.target.value }))}
/>
</div>
</div>
</div>
</div>
@@ -797,20 +820,32 @@ export function UserDetailModal({
</div>
</div>
</div>
{/* 存客宝标签 */}
{user.ckbTags && (
<div className="p-4 bg-[#0a1628] rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Tag className="w-4 h-4 text-purple-400" />
<span className="text-white font-medium"></span>
{/* 存客宝标签(与用户标签共用 ckb_tags兼容 JSON 与逗号分隔) */}
{(() => {
const raw = user.tags || user.ckbTags || ''
let arr: string[] = []
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw || '[]') : []
arr = Array.isArray(parsed) ? parsed : (typeof raw === 'string' ? raw.split(',') : [])
} catch {
arr = typeof raw === 'string' ? raw.split(',') : []
}
const tags = arr.map((t) => String(t).trim()).filter(Boolean)
if (tags.length === 0) return null
return (
<div className="p-4 bg-[#0a1628] rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Tag className="w-4 h-4 text-purple-400" />
<span className="text-white font-medium"></span>
</div>
<div className="flex flex-wrap gap-2">
{tags.map((tag, i) => (
<Badge key={i} className="bg-purple-500/20 text-purple-400 border-0">{tag}</Badge>
))}
</div>
</div>
<div className="flex flex-wrap gap-2">
{(typeof user.ckbTags === 'string' ? user.ckbTags.split(',') : []).map((tag, i) => (
<Badge key={i} className="bg-purple-500/20 text-purple-400 border-0">{tag.trim()}</Badge>
))}
</div>
</div>
)}
)
})()}
</TabsContent>
{/* ===== 用户旅程(原行为轨迹)===== */}

View File

@@ -181,6 +181,10 @@ export function DashboardPage() {
const formatOrderProduct = (p: OrderRow) => {
const type = p.productType || ''
const desc = p.description || ''
if (type === 'balance_recharge') {
const amount = typeof p.amount === 'number' ? p.amount.toFixed(2) : parseFloat(String(p.amount || '0')).toFixed(2)
return { title: `余额充值 ¥${amount}`, subtitle: '余额充值' }
}
if (desc) {
if (type === 'section' && desc.includes('章节')) {
if (desc.includes('-')) {
@@ -197,6 +201,9 @@ export function DashboardPage() {
if (type === 'fullbook' || desc.includes('全书')) {
return { title: '《一场Soul的创业实验》', subtitle: '全书购买' }
}
if (type === 'vip' || desc.includes('VIP')) {
return { title: '超级个体开通费用', subtitle: '超级个体' }
}
if (type === 'match' || desc.includes('伙伴')) {
return { title: '找伙伴匹配', subtitle: '功能服务' }
}
@@ -207,6 +214,7 @@ export function DashboardPage() {
}
if (type === 'section') return { title: `章节 ${p.productId || ''}`, subtitle: '单章购买' }
if (type === 'fullbook') return { title: '《一场Soul的创业实验》', subtitle: '全书购买' }
if (type === 'vip') return { title: '超级个体开通费用', subtitle: '超级个体' }
if (type === 'match') return { title: '找伙伴匹配', subtitle: '功能服务' }
return { title: '未知商品', subtitle: type || '其他' }
}

View File

@@ -838,6 +838,14 @@ export function DistributionPage() {
<p className="text-white text-sm">
{(() => {
const type = order.productType || order.type
const desc = order.description || ''
const pid = String(order.productId || order.sectionId || '')
const isVip = type === 'vip' || desc.includes('VIP') || desc.toLowerCase().includes('vip') || pid.toLowerCase().includes('vip')
if (type === 'balance_recharge') {
const amount = typeof order.amount === 'number' ? order.amount.toFixed(2) : parseFloat(String(order.amount || '0')).toFixed(2)
return `余额充值 ¥${amount}`
}
if (isVip) return '超级个体开通费用'
if (type === 'fullbook')
return `${order.bookName || '《底层逻辑》'} - 全本`
if (type === 'match') return '匹配次数购买'
@@ -847,6 +855,11 @@ export function DistributionPage() {
<p className="text-gray-500 text-xs">
{(() => {
const type = order.productType || order.type
const desc = order.description || ''
const pid = String(order.productId || order.sectionId || '')
const isVip = type === 'vip' || desc.includes('VIP') || desc.toLowerCase().includes('vip') || pid.toLowerCase().includes('vip')
if (type === 'balance_recharge') return '余额充值'
if (isVip) return '超级个体'
if (type === 'fullbook') return '全书解锁'
if (type === 'match') return '功能权益'
return order.chapterTitle || '单章购买'
@@ -860,9 +873,11 @@ export function DistributionPage() {
<td className="p-4 text-gray-300">
{order.paymentMethod === 'wechat'
? '微信支付'
: order.paymentMethod === 'alipay'
? '支付'
: order.paymentMethod || '微信支付'}
: order.paymentMethod === 'balance'
? '余额支付'
: order.paymentMethod === 'alipay'
? '支付宝'
: order.paymentMethod || '微信支付'}
</td>
<td className="p-4">
{order.status === 'refunded' ? (

View File

@@ -41,6 +41,9 @@ interface Purchase {
productType?: string
description?: string
refundReason?: string
giftPayRequestId?: string
payerUserId?: string
payerNickname?: string
}
interface UsersItem {
@@ -114,6 +117,10 @@ export function OrdersPage() {
const formatProduct = (order: Purchase) => {
const type = order.productType || order.type || ''
const desc = order.description || ''
if (type === 'balance_recharge') {
const amount = Number(order.amount || 0).toFixed(2)
return { name: `余额充值 ¥${amount}`, type: '余额充值' }
}
if (desc) {
if (type === 'section' && desc.includes('章节')) {
if (desc.includes('-')) {
@@ -128,7 +135,7 @@ export function OrdersPage() {
return { name: '《一场Soul的创业实验》', type: '全书购买' }
}
if (type === 'vip' || desc.includes('VIP')) {
return { name: 'VIP年度会员', type: 'VIP' }
return { name: '超级个体开通费用', type: '超级个体' }
}
if (type === 'match' || desc.includes('伙伴')) {
return { name: '找伙伴匹配', type: '功能服务' }
@@ -137,7 +144,7 @@ export function OrdersPage() {
}
if (type === 'section') return { name: `章节 ${order.productId || order.sectionId || ''}`, type: '单章' }
if (type === 'fullbook') return { name: '《一场Soul的创业实验》', type: '全书' }
if (type === 'vip') return { name: 'VIP年度会员', type: 'VIP' }
if (type === 'vip') return { name: '超级个体开通费用', type: '超级个体' }
if (type === 'match') return { name: '找伙伴匹配', type: '功能' }
return { name: '未知商品', type: type || '其他' }
}
@@ -182,7 +189,7 @@ export function OrdersPage() {
getUserPhone(p.userId),
product.name,
Number(p.amount || 0).toFixed(2),
p.paymentMethod === 'wechat' ? '微信支付' : p.paymentMethod === 'alipay' ? '支付宝' : p.paymentMethod || '微信支付',
p.paymentMethod === 'wechat' ? '微信支付' : p.paymentMethod === 'balance' ? '余额支付' : p.paymentMethod === 'alipay' ? '支付宝' : p.paymentMethod || '微信支付',
p.status === 'refunded' ? '已退款' : p.status === 'paid' || p.status === 'completed' ? '已完成' : p.status === 'pending' || p.status === 'created' ? '待支付' : '已失败',
p.status === 'refunded' && p.refundReason ? p.refundReason : '-',
p.referrerEarnings ? Number(p.referrerEarnings).toFixed(2) : '-',
@@ -305,8 +312,18 @@ export function OrdersPage() {
</TableCell>
<TableCell>
<div>
<p className="text-white text-sm">{getUserNickname(purchase)}</p>
<p className="text-white text-sm flex items-center gap-2">
{getUserNickname(purchase)}
{purchase.payerUserId && (
<Badge className="bg-amber-500/20 text-amber-400 hover:bg-amber-500/20 border-0 text-xs">
</Badge>
)}
</p>
<p className="text-gray-500 text-xs">{getUserPhone(purchase.userId)}</p>
{purchase.payerUserId && purchase.payerNickname && (
<p className="text-amber-400/80 text-xs mt-0.5">{purchase.payerNickname}</p>
)}
</div>
</TableCell>
<TableCell>
@@ -315,7 +332,7 @@ export function OrdersPage() {
{product.name}
{(purchase.productType || purchase.type) === 'vip' && (
<Badge className="bg-amber-500/20 text-amber-400 hover:bg-amber-500/20 border-0 text-xs">
VIP
</Badge>
)}
</p>
@@ -328,9 +345,11 @@ export function OrdersPage() {
<TableCell className="text-gray-300">
{purchase.paymentMethod === 'wechat'
? '微信支付'
: purchase.paymentMethod === 'alipay'
? '支付'
: purchase.paymentMethod || '微信支付'}
: purchase.paymentMethod === 'balance'
? '余额支付'
: purchase.paymentMethod === 'alipay'
? '支付宝'
: purchase.paymentMethod || '微信支付'}
</TableCell>
<TableCell>
{purchase.status === 'refunded' ? (
@@ -363,7 +382,8 @@ export function OrdersPage() {
{new Date(purchase.createdAt).toLocaleString('zh-CN')}
</TableCell>
<TableCell>
{(purchase.status === 'paid' || purchase.status === 'completed') && (
{(purchase.status === 'paid' || purchase.status === 'completed') &&
purchase.paymentMethod !== 'balance' && (
<Button
variant="outline"
size="sm"