Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev

This commit is contained in:
超级老白兔
2025-07-30 17:36:18 +08:00
8 changed files with 181 additions and 89 deletions

View File

@@ -1,4 +1,4 @@
# 基础环境变量示例
VITE_API_BASE_URL=http://www.yishi.com
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
# VITE_API_BASE_URL=http://www.yishi.com
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
VITE_APP_TITLE=Nkebao Base

View File

@@ -1,7 +1,15 @@
import React, { useEffect, useState, useCallback, useRef } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile";
import { SettingOutlined, RedoOutlined } from "@ant-design/icons";
import {
NavBar,
Tabs,
Switch,
Toast,
SpinLoading,
Button,
Avatar,
} from "antd-mobile";
import { SettingOutlined, RedoOutlined, UserOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import {
fetchDeviceDetail,
@@ -262,15 +270,32 @@ const DeviceDetail: React.FC = () => {
navigate(`/wechat-accounts/detail/${acc.wechatId}`);
}}
>
<img
src={acc.avatar || "/placeholder.svg"}
<Avatar
src={acc.avatar}
alt={acc.nickname}
style={{
width: 40,
height: 40,
borderRadius: 20,
background: "#eee",
}}
fallback={
<div
style={{
width: 40,
height: 40,
display: "flex",
alignItems: "center",
justifyContent: "center",
background:
"linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "white",
fontSize: 16,
borderRadius: "50%",
}}
>
<UserOutlined />
</div>
}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>{acc.nickname}</div>

View File

@@ -1,13 +1,7 @@
import request from "@/api/request";
import type {
TrafficPoolUserDetail,
UserJourneyResponse,
UserTagsResponse,
} from "./data";
import type { UserTagsResponse } from "./data";
export function getTrafficPoolDetail(
wechatId: string,
): Promise<TrafficPoolUserDetail> {
export function getTrafficPoolDetail(wechatId: string) {
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
}
@@ -16,7 +10,7 @@ export function getUserJourney(params: {
page: number;
pageSize: number;
userId: string;
}): Promise<UserJourneyResponse> {
}) {
return request("/v1/traffic/pool/getUserJourney", params, "GET");
}

View File

@@ -30,3 +30,79 @@ export interface TrafficPoolUserDetail {
value?: number;
}>;
}
// 扩展的用户详情类型
export interface ExtendedUserDetail extends TrafficPoolUserDetail {
userInfo: {
nickname: string;
avatar: string;
wechatId: string;
friendShip: {
totalFriend: number;
maleFriend: number;
femaleFriend: number;
unknowFriend: number;
};
};
rfmScore: {
recency: number;
frequency: number;
monetary: number;
totalScore: number;
};
trafficPools: {
currentPool: string;
availablePools: string[];
};
userTags: Array<{
id: string;
name: string;
color: string;
type: string;
}>;
valueTags: Array<{
id: string;
name: string;
color: string;
icon: string;
rfmScore: number;
valueLevel: string;
}>;
restrictions?: Array<{
id: string;
reason: string;
level: number;
date: number | null;
}>;
}
// 互动记录类型
export interface InteractionRecord {
id: string;
type: string;
content: string;
timestamp: string;
value?: number;
}
// 用户旅程记录类型
export interface UserJourneyRecord {
id: string;
type: number;
remark: string;
createTime: string;
}
// 用户标签响应类型
export interface UserTagsResponse {
wechat: string[];
siteLabels: UserTagItem[];
}
// 用户标签项类型
export interface UserTagItem {
id: string;
name: string;
color?: string;
type?: string;
}

View File

@@ -47,6 +47,18 @@
flex-shrink: 0;
}
.avatarFallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 24px;
border-radius: 50%;
}
.userDetails {
flex: 1;
min-width: 0;
@@ -343,7 +355,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 16px;
padding: 20px 16px;
text-align: center;
}
@@ -353,14 +365,14 @@
}
.emptyText {
font-size: 16px;
font-size: 14px;
color: #666;
margin-bottom: 8px;
margin-bottom: 4px;
font-weight: 500;
}
.emptyDesc {
font-size: 14px;
font-size: 12px;
color: #999;
line-height: 1.4;
}

View File

@@ -1,20 +1,9 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Card,
Button,
Avatar,
Tag,
Tabs,
List,
Badge,
SpinLoading,
} from "antd-mobile";
import { Card, Button, Avatar, Tag, List, SpinLoading } from "antd-mobile";
import {
UserOutlined,
CrownOutlined,
PlusOutlined,
CloseOutlined,
EyeOutlined,
DollarOutlined,
MobileOutlined,
@@ -26,9 +15,7 @@ import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api";
import type {
TrafficPoolUserDetail,
ExtendedUserDetail,
InteractionRecord,
UserJourneyRecord,
UserTagsResponse,
UserTagItem,
@@ -52,6 +39,7 @@ const TrafficPoolDetail: React.FC = () => {
// 用户标签相关状态
const [tagsLoading, setTagsLoading] = useState(false);
const [userTagsList, setUserTagsList] = useState<UserTagItem[]>([]);
const [wechatTagsList, setWechatTagsList] = useState<string[]>([]);
useEffect(() => {
if (!wxid) return;
@@ -61,6 +49,8 @@ const TrafficPoolDetail: React.FC = () => {
// 将API数据转换为扩展的用户详情数据
const extendedUser: ExtendedUserDetail = {
...res,
// 添加userInfo属性
userInfo: res.userInfo,
// 模拟RFM评分数据
rfmScore: {
recency: 5,
@@ -92,6 +82,8 @@ const TrafficPoolDetail: React.FC = () => {
},
],
};
console.log(extendedUser);
setUser(extendedUser);
})
.finally(() => setLoading(false));
@@ -131,6 +123,7 @@ const TrafficPoolDetail: React.FC = () => {
try {
const response: UserTagsResponse = await getUserTags(userId);
setUserTagsList(response.siteLabels || []);
setWechatTagsList(response.wechat || []);
} catch (error) {
console.error("获取用户标签失败:", error);
} finally {
@@ -149,10 +142,6 @@ const TrafficPoolDetail: React.FC = () => {
}
};
const handleClose = () => {
navigate(-1);
};
const getJourneyTypeIcon = (type: number) => {
switch (type) {
case 0: // 浏览
@@ -207,32 +196,6 @@ const TrafficPoolDetail: React.FC = () => {
}
};
const formatCurrency = (amount: number) => {
return `¥${amount.toLocaleString()}`;
};
const getGenderText = (gender: number) => {
switch (gender) {
case 1:
return "男";
case 2:
return "女";
default:
return "未知";
}
};
const getGenderColor = (gender: number) => {
switch (gender) {
case 1:
return "#1677ff";
case 2:
return "#eb2f96";
default:
return "#999";
}
};
const getRestrictionLevelText = (level: number) => {
switch (level) {
case 1:
@@ -297,7 +260,11 @@ const TrafficPoolDetail: React.FC = () => {
<Avatar
src={user.userInfo.avatar}
className={styles.avatar}
fallback={<UserOutlined />}
fallback={
<div className={styles.avatarFallback}>
<UserOutlined />
</div>
}
/>
<div className={styles.userDetails}>
<div className={styles.nickname}>{user.userInfo.nickname}</div>
@@ -617,20 +584,22 @@ const TrafficPoolDetail: React.FC = () => {
{activeTab === "tags" && (
<div className={styles.tabContent}>
{/* 用户标签 */}
<Card title="用户标签" className={styles.infoCard}>
{/* 站内标签 */}
<Card title="站内标签" className={styles.infoCard}>
{tagsLoading && userTagsList.length === 0 ? (
<div className={styles.loadingContainer}>
<SpinLoading color="primary" style={{ fontSize: 24 }} />
<SpinLoading color="primary" style={{ fontSize: 20 }} />
<div className={styles.loadingText}>...</div>
</div>
) : userTagsList.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<TagOutlined style={{ fontSize: 48, color: "#ccc" }} />
<TagOutlined style={{ fontSize: 36, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}></div>
</div>
) : (
<div className={styles.tagsSection}>
@@ -648,6 +617,39 @@ const TrafficPoolDetail: React.FC = () => {
)}
</Card>
{/* 微信标签 */}
<Card title="微信标签" className={styles.infoCard}>
{tagsLoading && wechatTagsList.length === 0 ? (
<div className={styles.loadingContainer}>
<SpinLoading color="primary" style={{ fontSize: 24 }} />
<div className={styles.loadingText}>...</div>
</div>
) : wechatTagsList.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>
<TagOutlined style={{ fontSize: 48, color: "#ccc" }} />
</div>
<div className={styles.emptyText}></div>
<div className={styles.emptyDesc}>
</div>
</div>
) : (
<div className={styles.tagsSection}>
{wechatTagsList.map((tag, index) => (
<Tag
key={index}
color="danger"
fill="outline"
className={styles.tagItem}
>
{tag}
</Tag>
))}
</div>
)}
</Card>
{/* 价值标签 */}
<Card title="价值标签" className={styles.infoCard}>
{user.valueTags && user.valueTags.length > 0 ? (

View File

@@ -1,6 +1,4 @@
import request from "@/api/request";
import type { TrafficPoolListResponse, DeviceOption } from "./data";
import { fetchDeviceList } from "@/pages/guide/api";
// 获取流量池列表
export function fetchTrafficPoolList(params: {
@@ -11,16 +9,6 @@ export function fetchTrafficPoolList(params: {
return request("/v1/traffic/pool", params, "GET");
}
// 获取设备列表(真实接口)
export async function fetchDeviceOptions(): Promise<DeviceOption[]> {
const res = await fetchDeviceList({ page: 1, limit: 100 });
// 假设返回 { list: [{ id, name, ... }], ... }
return (res.list || []).map((item: any) => ({
id: String(item.id),
name: item.name,
}));
}
// 获取分组列表如无真实接口可用mock
export async function fetchPackageOptions(): Promise<any[]> {
// TODO: 替换为真实接口

View File

@@ -1,9 +1,5 @@
import { useState, useEffect, useMemo } from "react";
import {
fetchTrafficPoolList,
fetchDeviceOptions,
fetchPackageOptions,
} from "./api";
import { fetchTrafficPoolList, fetchPackageOptions } from "./api";
import type {
TrafficPoolUser,
DeviceOption,
@@ -69,7 +65,6 @@ export function useTrafficPoolListLogic() {
// 获取筛选项
useEffect(() => {
fetchDeviceOptions().then(setDeviceOptions);
fetchPackageOptions().then(setPackageOptions);
}, []);