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:
@@ -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>
|
||||
|
||||
{/* ===== 用户旅程(原行为轨迹)===== */}
|
||||
|
||||
@@ -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 || '其他' }
|
||||
}
|
||||
|
||||
@@ -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' ? (
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user