Merge branch 'yongxu-dev3' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongxu-dev3
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=http://www.yishi.com
|
||||
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
|
||||
@@ -21,12 +21,7 @@ export async function uploadFile(
|
||||
},
|
||||
});
|
||||
|
||||
// 检查响应结果
|
||||
if (res?.code === 200 && res?.data?.url) {
|
||||
return res.data.url;
|
||||
} else {
|
||||
throw new Error(res?.msg || "文件上传失败");
|
||||
}
|
||||
return res.url;
|
||||
} catch (e: any) {
|
||||
throw new Error(e?.message || "文件上传失败");
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function FriendSelection({
|
||||
}
|
||||
|
||||
if (enableDeviceFilter && deviceIds.length > 0) {
|
||||
params.deviceIds = deviceIds;
|
||||
params.deviceIds = deviceIds.join(",");
|
||||
}
|
||||
|
||||
const response = await getFriendList(params);
|
||||
|
||||
@@ -11,7 +11,7 @@ const StepIndicator: React.FC<StepIndicatorProps> = ({
|
||||
steps,
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ marginBottom: 24, overflowX: "auto" }}>
|
||||
<div style={{ overflowX: "auto", padding: "30px 0px", background: "#fff" }}>
|
||||
<Steps current={currentStep - 1}>
|
||||
{steps.map((step, idx) => (
|
||||
<Steps.Step
|
||||
|
||||
@@ -46,81 +46,11 @@ const Home: React.FC = () => {
|
||||
const [dashboard, setDashboard] = useState<DashboardData>({});
|
||||
const [sevenDayStats, setSevenDayStats] = useState<SevenDayStatsData>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [apiError, setApiError] = useState("");
|
||||
|
||||
// 场景获客数据
|
||||
const scenarioFeatures = [
|
||||
{
|
||||
id: "3",
|
||||
name: "抖音获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-QR8ManuDplYTySUJsY4mymiZkDYnQ9.png",
|
||||
color: "bg-blue-100 text-blue-600",
|
||||
value: 156,
|
||||
growth: 12,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "小红书获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||||
color: "bg-red-100 text-red-600",
|
||||
value: 89,
|
||||
growth: 8,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "公众号获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-Gsg0CMf5tsZb41mioszdjqU1WmsRxW.png",
|
||||
color: "bg-green-100 text-green-600",
|
||||
value: 234,
|
||||
growth: 15,
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
name: "海报获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-x92XJgXy4MI7moNYlA1EAes2FqDxMH.png",
|
||||
color: "bg-orange-100 text-orange-600",
|
||||
value: 167,
|
||||
growth: 10,
|
||||
},
|
||||
];
|
||||
|
||||
// 今日数据统计
|
||||
const todayStatsData = [
|
||||
{
|
||||
title: "朋友圈同步",
|
||||
value: "12",
|
||||
icon: <MessageOutlined style={{ fontSize: 16, color: "#8b5cf6" }} />,
|
||||
color: "text-purple-600",
|
||||
path: "/workspace/moments-sync",
|
||||
},
|
||||
{
|
||||
title: "群发任务",
|
||||
value: "8",
|
||||
icon: <TeamOutlined style={{ fontSize: 16, color: "#f97316" }} />,
|
||||
color: "text-orange-600",
|
||||
path: "/workspace/group-push",
|
||||
},
|
||||
{
|
||||
title: "获客转化",
|
||||
value: "85%",
|
||||
icon: <RiseOutlined style={{ fontSize: 16, color: "#22c55e" }} />,
|
||||
color: "text-green-600",
|
||||
path: "/scenarios",
|
||||
},
|
||||
{
|
||||
title: "系统活跃度",
|
||||
value: "98%",
|
||||
icon: <LineChartOutlined style={{ fontSize: 16, color: "#3b82f6" }} />,
|
||||
color: "text-blue-600",
|
||||
path: "/workspace",
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setApiError("");
|
||||
|
||||
// 并行请求多个接口
|
||||
const [dashboardResult, planStatsResult, sevenDayResult, todayResult] =
|
||||
@@ -136,7 +66,6 @@ const Home: React.FC = () => {
|
||||
setDashboard(dashboardResult.value);
|
||||
} else {
|
||||
console.warn("仪表板API失败:", dashboardResult.reason);
|
||||
setApiError("API连接异常,显示默认数据");
|
||||
}
|
||||
|
||||
// 处理计划统计数据
|
||||
@@ -163,18 +92,21 @@ const Home: React.FC = () => {
|
||||
<MessageOutlined style={{ fontSize: 16, color: "#8b5cf6" }} />
|
||||
),
|
||||
color: "#8b5cf6",
|
||||
path: "/workspace/moments-sync",
|
||||
},
|
||||
{
|
||||
label: "群发任务",
|
||||
value: todayResult.value?.groupPushNum || 0,
|
||||
icon: <TeamOutlined style={{ fontSize: 16, color: "#f97316" }} />,
|
||||
color: "#f97316",
|
||||
path: "/workspace/group-push",
|
||||
},
|
||||
{
|
||||
label: "获客转化率",
|
||||
value: todayResult.value?.passRate || "0%",
|
||||
icon: <RiseOutlined style={{ fontSize: 16, color: "#22c55e" }} />,
|
||||
color: "#22c55e",
|
||||
path: "/scenarios",
|
||||
},
|
||||
{
|
||||
label: "系统活跃度",
|
||||
@@ -183,6 +115,7 @@ const Home: React.FC = () => {
|
||||
<LineChartOutlined style={{ fontSize: 16, color: "#3b82f6" }} />
|
||||
),
|
||||
color: "#3b82f6",
|
||||
path: "/workspace",
|
||||
},
|
||||
];
|
||||
setTodayStats(todayStatsData);
|
||||
@@ -191,7 +124,6 @@ const Home: React.FC = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取数据失败:", error);
|
||||
setApiError(error instanceof Error ? error.message : "数据加载失败");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -208,38 +140,6 @@ const Home: React.FC = () => {
|
||||
navigate("/wechat-accounts");
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>
|
||||
<span className={style["nav-text"]}>存客宝</span>
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
loading={true}
|
||||
>
|
||||
<div className={style["home-page"]}>
|
||||
<div className={style["content-wrapper"]}>
|
||||
<div className={style["stats-grid"]}>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className={style["stat-card"]}>
|
||||
<div className={style["stat-label"]}></div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span></span>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
@@ -249,7 +149,8 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
footer={<MeauMobile activeKey="home" />}
|
||||
loading={isLoading}
|
||||
>
|
||||
<div className={style["home-page"]}>
|
||||
<div className={style["content-wrapper"]}>
|
||||
@@ -298,32 +199,29 @@ const Home: React.FC = () => {
|
||||
<h2 className={style["section-title"]}>场景获客统计</h2>
|
||||
</div>
|
||||
<div className={style["scene-grid"]}>
|
||||
{scenarioFeatures
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 4) // 只显示前4个
|
||||
.map((scenario) => (
|
||||
<div
|
||||
key={scenario.id}
|
||||
className={style["scene-item"]}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/scenarios/list/${scenario.id}/${encodeURIComponent(
|
||||
scenario.name
|
||||
)}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={style["scene-icon"]}>
|
||||
<img
|
||||
src={scenario.icon || "/placeholder.svg"}
|
||||
alt={scenario.name}
|
||||
className={style["scene-image"]}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["scene-value"]}>{scenario.value}</div>
|
||||
<div className={style["scene-label"]}>{scenario.name}</div>
|
||||
{sceneStats.map((scenario) => (
|
||||
<div
|
||||
key={scenario.id}
|
||||
className={style["scene-item"]}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/scenarios/list/${scenario.id}/${encodeURIComponent(
|
||||
scenario.name
|
||||
)}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={style["scene-icon"]}>
|
||||
<img
|
||||
src={scenario.image || "/placeholder.svg"}
|
||||
alt={scenario.name}
|
||||
className={style["scene-image"]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={style["scene-value"]}>{scenario.allNum}</div>
|
||||
<div className={style["scene-label"]}>{scenario.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -333,7 +231,7 @@ const Home: React.FC = () => {
|
||||
<h2 className={style["section-title"]}>今日数据</h2>
|
||||
</div>
|
||||
<div className={style["today-grid"]}>
|
||||
{todayStatsData.map((stat, index) => (
|
||||
{todayStats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={style["today-item"]}
|
||||
@@ -342,7 +240,7 @@ const Home: React.FC = () => {
|
||||
<div className={style["today-icon"]}>{stat.icon}</div>
|
||||
<div>
|
||||
<div className={style["today-value"]}>{stat.value}</div>
|
||||
<div className={style["today-label"]}>{stat.title}</div>
|
||||
<div className={style["today-label"]}>{stat.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronRight, Settings, Bell, LogOut, Smartphone, MessageCircle, Database, FolderOpen } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import UnifiedHeader from '@/components/UnifiedHeader';
|
||||
import '@/components/Layout.css';
|
||||
|
||||
export default function Profile() {
|
||||
const navigate = useNavigate();
|
||||
const { user, logout, isAuthenticated } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [stats, setStats] = useState({
|
||||
devices: 12,
|
||||
wechat: 25,
|
||||
traffic: 8,
|
||||
content: 156,
|
||||
});
|
||||
|
||||
// 从localStorage获取用户信息
|
||||
useEffect(() => {
|
||||
const userInfoStr = localStorage.getItem('userInfo');
|
||||
if (userInfoStr) {
|
||||
setUserInfo(JSON.parse(userInfoStr));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 用户信息
|
||||
const currentUserInfo = {
|
||||
name: userInfo?.username || user?.username || "卡若",
|
||||
email: userInfo?.email || "zhangsan@example.com",
|
||||
role: "管理员",
|
||||
joinDate: "2023-01-15",
|
||||
lastLogin: "2024-01-20 14:30",
|
||||
};
|
||||
|
||||
// 功能模块数据
|
||||
const functionModules = [
|
||||
{
|
||||
id: "devices",
|
||||
title: "设备管理",
|
||||
description: "管理您的设备和微信账号",
|
||||
icon: <Smartphone className="h-5 w-5 text-blue-500" />,
|
||||
count: stats.devices,
|
||||
path: "/devices",
|
||||
bgColor: "bg-blue-50",
|
||||
},
|
||||
{
|
||||
id: "wechat",
|
||||
title: "微信号管理",
|
||||
description: "管理微信账号和好友",
|
||||
icon: <MessageCircle className="h-5 w-5 text-green-500" />,
|
||||
count: stats.wechat,
|
||||
path: "/wechat-accounts",
|
||||
bgColor: "bg-green-50",
|
||||
},
|
||||
{
|
||||
id: "traffic",
|
||||
title: "流量池",
|
||||
description: "管理用户流量池和分组",
|
||||
icon: <Database className="h-5 w-5 text-purple-500" />,
|
||||
count: stats.traffic,
|
||||
path: "/traffic-pool",
|
||||
bgColor: "bg-purple-50",
|
||||
},
|
||||
{
|
||||
id: "content",
|
||||
title: "内容库",
|
||||
description: "管理营销内容和素材",
|
||||
icon: <FolderOpen className="h-5 w-5 text-orange-500" />,
|
||||
count: stats.content,
|
||||
path: "/content",
|
||||
bgColor: "bg-orange-50",
|
||||
},
|
||||
];
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
// 这里可以调用实际的API
|
||||
// const [deviceStats, wechatStats, trafficStats, contentStats] = await Promise.allSettled([
|
||||
// getDeviceStats(),
|
||||
// getWechatStats(),
|
||||
// getTrafficStats(),
|
||||
// getContentStats(),
|
||||
// ]);
|
||||
|
||||
// 暂时使用模拟数据
|
||||
setStats({
|
||||
devices: 12,
|
||||
wechat: 25,
|
||||
traffic: 8,
|
||||
content: 156,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载统计数据失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
// 清除本地存储的用户信息
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('token_expired');
|
||||
localStorage.removeItem('s2_accountId');
|
||||
localStorage.removeItem('userInfo');
|
||||
setShowLogoutDialog(false);
|
||||
logout();
|
||||
navigate('/login');
|
||||
toast({
|
||||
title: '退出成功',
|
||||
description: '您已安全退出系统',
|
||||
});
|
||||
};
|
||||
|
||||
const handleFunctionClick = (path: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="text-gray-500">请先登录</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<UnifiedHeader
|
||||
title="我的"
|
||||
showBack={false}
|
||||
titleColor="blue"
|
||||
actions={[
|
||||
{
|
||||
type: 'icon',
|
||||
icon: Bell,
|
||||
onClick: () => console.log('Notifications'),
|
||||
},
|
||||
{
|
||||
type: 'icon',
|
||||
icon: Settings,
|
||||
onClick: () => console.log('Settings'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gray-50 pb-16">
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 用户信息卡片 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage src={userInfo?.avatar || user?.avatar || ''} />
|
||||
<AvatarFallback className="bg-gray-200 text-gray-600 text-lg font-medium">
|
||||
{currentUserInfo.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<h2 className="text-lg font-medium">{currentUserInfo.name}</h2>
|
||||
<span className="px-2 py-1 text-xs bg-gradient-to-r from-orange-400 to-orange-500 text-white rounded-full font-medium shadow-sm">
|
||||
{currentUserInfo.role}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">{currentUserInfo.email}</p>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div>最近登录: {currentUserInfo.lastLogin}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 我的功能 */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-2">
|
||||
{functionModules.map((module) => (
|
||||
<div
|
||||
key={module.id}
|
||||
className="flex items-center p-4 rounded-lg border hover:bg-gray-50 cursor-pointer transition-colors w-full"
|
||||
onClick={() => handleFunctionClick(module.path)}
|
||||
>
|
||||
<div className={`p-2 rounded-lg ${module.bgColor} mr-3`}>{module.icon}</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{module.title}</div>
|
||||
<div className="text-xs text-gray-500">{module.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2 py-1 text-xs bg-gray-50 text-gray-700 rounded-full border border-gray-200 font-medium shadow-sm">
|
||||
{module.count}
|
||||
</span>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 退出登录 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-red-600 border-red-200 hover:bg-red-50 bg-transparent"
|
||||
onClick={() => setShowLogoutDialog(true)}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 退出登录确认对话框 */}
|
||||
<Dialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认退出登录</DialogTitle>
|
||||
<DialogDescription>
|
||||
您确定要退出登录吗?退出后需要重新登录才能使用完整功能。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="outline" onClick={() => setShowLogoutDialog(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleLogout}>
|
||||
确认退出
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
import request from '@/api/request';
|
||||
|
||||
// 设备统计
|
||||
export function getDeviceStats() {
|
||||
return request('/v1/dashboard/device-stats', {}, 'GET');
|
||||
import request from "@/api/request";
|
||||
// 首页仪表盘总览
|
||||
export function getDashboard() {
|
||||
return request("/v1/dashboard", {}, "GET");
|
||||
}
|
||||
|
||||
// 微信号统计
|
||||
export function getWechatStats() {
|
||||
return request('/v1/dashboard/wechat-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 你可以根据需要继续添加其他接口
|
||||
// 例如:场景获客统计、今日数据统计等
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { getDashboard } from "./api";
|
||||
const Mine: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const { user } = useUserStore();
|
||||
const [stats, setStats] = useState({
|
||||
devices: 12,
|
||||
wechat: 25,
|
||||
@@ -25,21 +26,15 @@ const Mine: React.FC = () => {
|
||||
});
|
||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||
|
||||
// 从localStorage获取用户信息
|
||||
useEffect(() => {
|
||||
const userInfoStr = localStorage.getItem("userInfo");
|
||||
if (userInfoStr) {
|
||||
setUserInfo(JSON.parse(userInfoStr));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 用户信息
|
||||
const currentUserInfo = {
|
||||
name: userInfo?.username || "售前",
|
||||
email: userInfo?.email || "zhangsan@example.com",
|
||||
role: "管理员",
|
||||
lastLogin: "2024-01-20 14:30",
|
||||
avatar: userInfo?.avatar || "",
|
||||
name: user?.username || "-",
|
||||
email: user?.account || "-",
|
||||
role: user?.isAdmin === 1 ? "管理员" : "普通用户",
|
||||
lastLogin: user?.lastLoginTime
|
||||
? new Date(user.lastLoginTime * 1000).toLocaleString()
|
||||
: "-",
|
||||
avatar: user?.avatar || "",
|
||||
};
|
||||
|
||||
// 功能模块数据
|
||||
@@ -89,20 +84,12 @@ const Mine: React.FC = () => {
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
// 这里可以调用实际的API
|
||||
// const [deviceStats, wechatStats, trafficStats, contentStats] = await Promise.allSettled([
|
||||
// getDeviceStats(),
|
||||
// getWechatStats(),
|
||||
// getTrafficStats(),
|
||||
// getContentStats(),
|
||||
// ]);
|
||||
|
||||
// 暂时使用模拟数据
|
||||
const res = await getDashboard();
|
||||
setStats({
|
||||
devices: 12,
|
||||
wechat: 25,
|
||||
traffic: 8,
|
||||
content: 156,
|
||||
devices: res.deviceNum,
|
||||
wechat: res.wechatNum,
|
||||
traffic: 999,
|
||||
content: 999,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载统计数据失败:", error);
|
||||
@@ -196,7 +183,6 @@ const Mine: React.FC = () => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
<div className={style["user-name"]}>{currentUserInfo.name}</div>
|
||||
@@ -225,7 +211,6 @@ const Mine: React.FC = () => {
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
|
||||
>
|
||||
<BellOutlined style={{ fontSize: "20px", color: "#666" }} />
|
||||
<SettingOutlined style={{ fontSize: "20px", color: "#666" }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { message } from "antd";
|
||||
import { NavBar } from "antd-mobile";
|
||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import BasicSettings from "./steps/BasicSettings";
|
||||
import FriendRequestSettings from "./steps/FriendRequestSettings";
|
||||
import MessageSettings from "./steps/MessageSettings";
|
||||
@@ -205,25 +204,8 @@ export default function NewPlan() {
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div className="nav-title">
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => router(-1)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className="nav-title">
|
||||
{isEdit ? "编辑朋友圈同步" : "新建朋友圈同步"}
|
||||
</span>
|
||||
</NavBar>
|
||||
<div className="px-4 py-6">
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
</div>
|
||||
<NavCommon title={isEdit ? "编辑场景计划" : "新建场景计划"} />
|
||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Tag,
|
||||
Switch,
|
||||
// Upload,
|
||||
Modal,
|
||||
Alert,
|
||||
Row,
|
||||
Col,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Form, Input, Button, Tag, Switch, Modal, Spin } from "antd";
|
||||
import { Button as ButtonMobile } from "antd-mobile";
|
||||
import {
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
@@ -21,6 +10,7 @@ import {
|
||||
CheckOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { uploadFile } from "@/api/common";
|
||||
import styles from "./base.module.scss";
|
||||
|
||||
interface BasicSettingsProps {
|
||||
isEdit: boolean;
|
||||
@@ -424,31 +414,44 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
formData.scenario !== 1 ? { display: "none" } : { display: "block" };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles["basic-container"]}>
|
||||
{/* 场景选择区块 */}
|
||||
{sceneLoading ? (
|
||||
<div style={{ padding: 16, textAlign: "center" }}>加载中...</div>
|
||||
) : (
|
||||
<Row gutter={20} className="grid grid-cols-3 gap-4">
|
||||
{sceneList.map((scene) => (
|
||||
<Col span={8} key={scene.id}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleScenarioSelect(scene.id)}
|
||||
className={`w-full ${formData.scenario === scene.id ? "bg-blue-500" : "bg-gray-200"}`}
|
||||
>
|
||||
{scene.name.replace("获客", "")}
|
||||
</Button>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<div className={styles["basic-scene-select"]}>
|
||||
{sceneLoading ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: 80,
|
||||
}}
|
||||
>
|
||||
<Spin size="large"></Spin>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles["basic-scene-grid"]}>
|
||||
{sceneList.map((scene) => {
|
||||
const selected = formData.scenario === scene.id;
|
||||
return (
|
||||
<button
|
||||
key={scene.id}
|
||||
onClick={() => handleScenarioSelect(scene.id)}
|
||||
className={
|
||||
styles["basic-scene-btn"] +
|
||||
(selected ? " " + styles.selected : "")
|
||||
}
|
||||
>
|
||||
{scene.name.replace("获客", "")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 计划名称输入区 */}
|
||||
<div className="mb-4">计划名称</div>
|
||||
<div className="border p-2 mb-4">
|
||||
<div className={styles["basic-label"]}>计划名称</div>
|
||||
<div className={styles["basic-input-block"]}>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
onChange({ ...formData, name: String(e.target.value) })
|
||||
@@ -456,17 +459,16 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
placeholder="请输入计划名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">获客标签(可多选)</div>
|
||||
<div className={styles["basic-label"]}>获客标签(可多选)</div>
|
||||
{/* 标签选择区块 */}
|
||||
{formData.scenario && (
|
||||
<div className="flex pb-4" style={{ flexWrap: "wrap", gap: 8 }}>
|
||||
<div className={styles["basic-tag-list"]}>
|
||||
{(currentScene?.scenarioTags || []).map((tag: string) => (
|
||||
<Tag
|
||||
key={tag}
|
||||
color={selectedScenarioTags.includes(tag) ? "blue" : "default"}
|
||||
onClick={() => handleScenarioTagToggle(tag)}
|
||||
style={{ marginBottom: 4 }}
|
||||
className={styles["basic-tag-item"]}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
@@ -477,9 +479,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
key={tag.id}
|
||||
color={selectedScenarioTags.includes(tag.id) ? "blue" : "default"}
|
||||
onClick={() => handleScenarioTagToggle(tag.id)}
|
||||
style={{ marginBottom: 4 }}
|
||||
closable
|
||||
onClose={() => handleRemoveCustomTag(tag.id)}
|
||||
className={styles["basic-tag-item"]}
|
||||
>
|
||||
{tag.name}
|
||||
</Tag>
|
||||
@@ -487,49 +489,33 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{/* 自定义标签输入区 */}
|
||||
<div className="flex p" style={{ gap: 8 }}>
|
||||
<div className="border p-2 flex-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={customTagInput}
|
||||
onChange={(e) => setCustomTagInput(e.target.value)}
|
||||
placeholder="添加自定义标签"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<Button type="primary" onClick={handleAddCustomTag}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles["basic-custom-tag-input"]}>
|
||||
<Input
|
||||
type="text"
|
||||
value={customTagInput}
|
||||
onChange={(e) => setCustomTagInput(e.target.value)}
|
||||
placeholder="添加自定义标签"
|
||||
/>
|
||||
<Button type="primary" onClick={handleAddCustomTag}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 输入获客成功提示 */}
|
||||
<div className="flex pt-4">
|
||||
<div className="border p-2 flex-1">
|
||||
<Input
|
||||
type="text"
|
||||
value={tips}
|
||||
onChange={(e) => {
|
||||
setTips(e.target.value);
|
||||
onChange({ ...formData, tips: e.target.value });
|
||||
}}
|
||||
placeholder="请输入获客成功提示"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 选素材 */}
|
||||
<div className="my-4" style={openPoster}>
|
||||
<div className="mb-4">选择海报</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: 12,
|
||||
<div className={styles["basic-success-tip"]}>
|
||||
<Input
|
||||
type="text"
|
||||
value={tips}
|
||||
onChange={(e) => {
|
||||
setTips(e.target.value);
|
||||
onChange({ ...formData, tips: e.target.value });
|
||||
}}
|
||||
>
|
||||
placeholder="请输入获客成功提示"
|
||||
/>
|
||||
</div>
|
||||
{/* 选素材 */}
|
||||
<div className={styles["basic-materials"]} style={openPoster}>
|
||||
<div className={styles["basic-label"]}>选择海报</div>
|
||||
<div className={styles["basic-materials-grid"]}>
|
||||
{[...materials, ...customPosters].map((material) => {
|
||||
const isSelected = selectedMaterials.some(
|
||||
(m) => m.id === material.id
|
||||
@@ -538,35 +524,15 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
style={{
|
||||
border: isSelected ? "2px solid #1890ff" : "2px solid #eee",
|
||||
borderRadius: 8,
|
||||
padding: 6,
|
||||
cursor: "pointer",
|
||||
background: isSelected ? "#e6f7ff" : "#fff",
|
||||
transition: "border 0.2s",
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
height: 180 + 12, // 图片高度180+上下padding
|
||||
overflow: "hidden",
|
||||
minHeight: 192,
|
||||
}}
|
||||
className={
|
||||
styles["basic-material-card"] +
|
||||
(isSelected ? " " + styles.selected : "")
|
||||
}
|
||||
onClick={() => handleMaterialSelect(material)}
|
||||
>
|
||||
{/* 预览按钮:自定义海报在左上,内置海报在右上 */}
|
||||
<Button
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
left: isCustom ? 8 : "auto",
|
||||
right: isCustom ? "auto" : 8,
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
border: "none",
|
||||
borderRadius: "50%",
|
||||
padding: 2,
|
||||
zIndex: 2,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
<span
|
||||
className={styles["basic-material-preview"]}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePreviewImage(material.preview);
|
||||
@@ -575,7 +541,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
<EyeOutlined
|
||||
style={{ color: "#fff", width: 18, height: 18 }}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
{/* 删除自定义海报按钮 */}
|
||||
{isCustom && (
|
||||
<Button
|
||||
@@ -607,31 +573,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
<img
|
||||
src={material.preview}
|
||||
alt={material.name}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 180,
|
||||
objectFit: "cover",
|
||||
borderRadius: 4,
|
||||
marginBottom: 0,
|
||||
display: "block",
|
||||
}}
|
||||
className={styles["basic-material-img"]}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
padding: "4px 0",
|
||||
borderBottomLeftRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
textAlign: "center",
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<div className={styles["basic-material-name"]}>
|
||||
{material.name}
|
||||
</div>
|
||||
</div>
|
||||
@@ -639,19 +583,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
})}
|
||||
{/* 添加海报卡片 */}
|
||||
<div
|
||||
style={{
|
||||
border: "2px dashed #bbb",
|
||||
borderRadius: 8,
|
||||
padding: 6,
|
||||
cursor: "pointer",
|
||||
background: "#fafbfc",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: 190,
|
||||
}}
|
||||
className={styles["basic-add-material"]}
|
||||
onClick={() => uploadInputRef.current?.click()}
|
||||
>
|
||||
<span style={{ fontSize: 36, color: "#bbb", marginBottom: 8 }}>
|
||||
@@ -666,7 +598,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// 直接上传
|
||||
try {
|
||||
const url = await uploadFile(file);
|
||||
const newPoster = {
|
||||
@@ -675,9 +606,14 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
type: "poster",
|
||||
preview: url,
|
||||
};
|
||||
console.log(newPoster);
|
||||
|
||||
setCustomPosters((prev) => [...prev, newPoster]);
|
||||
setSelectedMaterials([newPoster]);
|
||||
onChange({ ...formData, materials: [newPoster] });
|
||||
} catch (err) {
|
||||
// 可加toast提示
|
||||
log;
|
||||
}
|
||||
e.target.value = "";
|
||||
}
|
||||
@@ -705,11 +641,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
</Modal>
|
||||
</div>
|
||||
{/* 订单导入区块优化 */}
|
||||
<div style={openOrder} className="my-4">
|
||||
<div style={{ fontWeight: 500, marginBottom: 8 }}>订单表格上传</div>
|
||||
<div style={{ display: "flex", gap: 12, marginBottom: 4 }}>
|
||||
<div className={styles["basic-order-upload"]} style={openOrder}>
|
||||
<div className={styles["basic-order-upload-label"]}>订单表格上传</div>
|
||||
<div className={styles["basic-order-upload-actions"]}>
|
||||
<Button
|
||||
type="button"
|
||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||
onClick={handleDownloadTemplate}
|
||||
>
|
||||
@@ -741,78 +676,68 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ color: "#888", fontSize: 13, marginBottom: 8 }}>
|
||||
<div className={styles["basic-order-upload-tip"]}>
|
||||
支持 CSV、Excel 格式,上传后将文件保存到服务器
|
||||
</div>
|
||||
</div>
|
||||
{/* 电话获客设置区块,仅在选择电话获客场景时显示 */}
|
||||
{formData.scenario === 5 && (
|
||||
<div style={{ margin: "16px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#f7f8fa",
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.03)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: 16, marginBottom: 16 }}>
|
||||
电话获客设置
|
||||
<div className={styles["basic-phone-settings"]}>
|
||||
<div style={{ fontWeight: 600, fontSize: 16, marginBottom: 16 }}>
|
||||
电话获客设置
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>自动加好友</span>
|
||||
<Switch
|
||||
checked={phoneSettings.autoAdd}
|
||||
onChange={(v) =>
|
||||
setPhoneSettings((s) => ({ ...s, autoAdd: v }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>自动加好友</span>
|
||||
<Switch
|
||||
checked={phoneSettings.autoAdd}
|
||||
onChange={(v) =>
|
||||
setPhoneSettings((s) => ({ ...s, autoAdd: v }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>语音转文字</span>
|
||||
<Switch
|
||||
checked={phoneSettings.speechToText}
|
||||
onChange={(v) =>
|
||||
setPhoneSettings((s) => ({ ...s, speechToText: v }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>问题提取</span>
|
||||
<Switch
|
||||
checked={phoneSettings.questionExtraction}
|
||||
onChange={(v) =>
|
||||
setPhoneSettings((s) => ({ ...s, questionExtraction: v }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>语音转文字</span>
|
||||
<Switch
|
||||
checked={phoneSettings.speechToText}
|
||||
onChange={(v) =>
|
||||
setPhoneSettings((s) => ({ ...s, speechToText: v }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>问题提取</span>
|
||||
<Switch
|
||||
checked={phoneSettings.questionExtraction}
|
||||
onChange={(v) =>
|
||||
setPhoneSettings((s) => ({ ...s, questionExtraction: v }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 微信群设置区块,仅在选择微信群场景时显示 */}
|
||||
{formData.scenario === 7 && (
|
||||
<div style={{ margin: "16px 0" }}>
|
||||
<div className={styles["basic-wechat-group"]}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Input
|
||||
value={weixinqunName}
|
||||
@@ -833,24 +758,19 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
margin: "16px 0",
|
||||
}}
|
||||
>
|
||||
<div className={styles["basic-footer-switch"]}>
|
||||
<span>是否启用</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onChange={(value) => onChange({ ...formData, enabled: value })}
|
||||
/>
|
||||
</div>
|
||||
<Button className="mt-4" type="primary" onClick={onNext}>
|
||||
下一步
|
||||
</Button>
|
||||
|
||||
<div className={styles["basic-footer-switch"]}>
|
||||
<ButtonMobile block color="primary" onClick={onNext}>
|
||||
下一步
|
||||
</ButtonMobile>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "antd";
|
||||
import { QuestionCircleOutlined, MessageOutlined } from "@ant-design/icons";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
|
||||
interface FriendRequestSettingsProps {
|
||||
formData: any;
|
||||
@@ -97,143 +98,151 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<span className="font-medium text-base">选择设备</span>
|
||||
<div className="mt-2">
|
||||
<DeviceSelection
|
||||
selectedDevices={selectedDevices.map((d) => d.id)}
|
||||
onSelect={(deviceIds) => {
|
||||
const newSelectedDevices = deviceIds.map((id) => ({
|
||||
id,
|
||||
name: `设备 ${id}`,
|
||||
status: "online",
|
||||
}));
|
||||
setSelectedDevices(newSelectedDevices);
|
||||
onChange({ ...formData, device: deviceIds });
|
||||
}}
|
||||
placeholder="选择设备"
|
||||
/>
|
||||
<Layout
|
||||
footer={
|
||||
<div className="p-4 border-t bg-white">
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={onPrev}>上一步</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-4 space-y-6">
|
||||
<div>
|
||||
<span className="font-medium text-base">选择设备</span>
|
||||
<div className="mt-2">
|
||||
<DeviceSelection
|
||||
selectedDevices={selectedDevices.map((d) => d.id)}
|
||||
onSelect={(deviceIds) => {
|
||||
const newSelectedDevices = deviceIds.map((id) => ({
|
||||
id,
|
||||
name: `设备 ${id}`,
|
||||
status: "online",
|
||||
}));
|
||||
setSelectedDevices(newSelectedDevices);
|
||||
onChange({ ...formData, device: deviceIds });
|
||||
}}
|
||||
placeholder="选择设备"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center space-x-2 mb-1 relative">
|
||||
<span className="font-medium text-base">好友备注</span>
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-gray-300 transition-colors"
|
||||
onMouseEnter={() => setShowRemarkTip(true)}
|
||||
onMouseLeave={() => setShowRemarkTip(false)}
|
||||
onClick={() => setShowRemarkTip((v) => !v)}
|
||||
>
|
||||
?
|
||||
</span>
|
||||
{showRemarkTip && (
|
||||
<div className="absolute left-24 top-0 z-20 w-64 p-3 bg-white border border-gray-200 rounded shadow-lg text-sm text-gray-700">
|
||||
<div>设置添加好友时的备注格式</div>
|
||||
<div className="mt-2 text-xs text-gray-500">备注格式预览:</div>
|
||||
<div className="mt-1 text-blue-600">
|
||||
{formData.remarkType === "phone" &&
|
||||
`138****1234+${getScenarioTitle()}`}
|
||||
{formData.remarkType === "nickname" &&
|
||||
`小红书用户2851+${getScenarioTitle()}`}
|
||||
{formData.remarkType === "source" &&
|
||||
`抖音直播+${getScenarioTitle()}`}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center space-x-2 mb-1 relative">
|
||||
<span className="font-medium text-base">好友备注</span>
|
||||
<span
|
||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-gray-300 transition-colors"
|
||||
onMouseEnter={() => setShowRemarkTip(true)}
|
||||
onMouseLeave={() => setShowRemarkTip(false)}
|
||||
onClick={() => setShowRemarkTip((v) => !v)}
|
||||
>
|
||||
?
|
||||
</span>
|
||||
{showRemarkTip && (
|
||||
<div className="absolute left-24 top-0 z-20 w-64 p-3 bg-white border border-gray-200 rounded shadow-lg text-sm text-gray-700">
|
||||
<div>设置添加好友时的备注格式</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
备注格式预览:
|
||||
</div>
|
||||
<div className="mt-1 text-blue-600">
|
||||
{formData.remarkType === "phone" &&
|
||||
`138****1234+${getScenarioTitle()}`}
|
||||
{formData.remarkType === "nickname" &&
|
||||
`小红书用户2851+${getScenarioTitle()}`}
|
||||
{formData.remarkType === "source" &&
|
||||
`抖音直播+${getScenarioTitle()}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
value={formData.remarkType || "phone"}
|
||||
onChange={(value) => onChange({ ...formData, remarkType: value })}
|
||||
className="w-full mt-2"
|
||||
>
|
||||
{remarkTypes.map((type) => (
|
||||
<Select.Option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-base">招呼语</span>
|
||||
<Button
|
||||
onClick={() => setIsTemplateDialogOpen(true)}
|
||||
className="text-blue-500"
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
value={formData.remarkType || "phone"}
|
||||
onChange={(value) => onChange({ ...formData, remarkType: value })}
|
||||
className="w-full mt-2"
|
||||
>
|
||||
<MessageOutlined className="h-4 w-4 mr-2" />
|
||||
参考模板
|
||||
</Button>
|
||||
{remarkTypes.map((type) => (
|
||||
<Select.Option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<Input
|
||||
value={formData.greeting}
|
||||
onChange={(e) =>
|
||||
onChange({ ...formData, greeting: e.target.value })
|
||||
}
|
||||
placeholder="请输入招呼语"
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-base">添加间隔</span>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-base">招呼语</span>
|
||||
<Button
|
||||
onClick={() => setIsTemplateDialogOpen(true)}
|
||||
className="text-blue-500"
|
||||
>
|
||||
<MessageOutlined className="h-4 w-4 mr-2" />
|
||||
参考模板
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.addFriendInterval || 1}
|
||||
value={formData.greeting}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...formData,
|
||||
addFriendInterval: Number(e.target.value),
|
||||
})
|
||||
onChange({ ...formData, greeting: e.target.value })
|
||||
}
|
||||
/>
|
||||
<div className="w-10">分钟</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-base">允许加人的时间段</span>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.addFriendTimeStart || "09:00"}
|
||||
onChange={(e) =>
|
||||
onChange({ ...formData, addFriendTimeStart: e.target.value })
|
||||
}
|
||||
className="w-32"
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.addFriendTimeEnd || "18:00"}
|
||||
onChange={(e) =>
|
||||
onChange({ ...formData, addFriendTimeEnd: e.target.value })
|
||||
}
|
||||
className="w-32"
|
||||
placeholder="请输入招呼语"
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasWarnings && (
|
||||
<Alert
|
||||
message="警告"
|
||||
description="您有未完成的设置项,建议完善后再进入下一步。"
|
||||
type="warning"
|
||||
showIcon
|
||||
className="bg-amber-50 border-amber-200"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium text-base">添加间隔</span>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.addFriendInterval || 1}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...formData,
|
||||
addFriendInterval: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="w-10">分钟</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button onClick={onPrev}>上一步</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
下一步
|
||||
</Button>
|
||||
<div>
|
||||
<span className="font-medium text-base">允许加人的时间段</span>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.addFriendTimeStart || "09:00"}
|
||||
onChange={(e) =>
|
||||
onChange({ ...formData, addFriendTimeStart: e.target.value })
|
||||
}
|
||||
className="w-32"
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.addFriendTimeEnd || "18:00"}
|
||||
onChange={(e) =>
|
||||
onChange({ ...formData, addFriendTimeEnd: e.target.value })
|
||||
}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasWarnings && (
|
||||
<Alert
|
||||
message="警告"
|
||||
description="您有未完成的设置项,建议完善后再进入下一步。"
|
||||
type="warning"
|
||||
showIcon
|
||||
className="bg-amber-50 border-amber-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<Modal
|
||||
open={isTemplateDialogOpen}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
LinkOutlined,
|
||||
TeamOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
|
||||
interface MessageContent {
|
||||
id: string;
|
||||
@@ -533,23 +534,29 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">消息设置</h2>
|
||||
<Button onClick={() => setIsAddDayPlanOpen(true)}>
|
||||
<PlusOutlined className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Layout
|
||||
footer={
|
||||
<div className="p-4 border-t bg-white">
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={onPrev}>上一步</Button>
|
||||
<Button type="primary" onClick={onNext}>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">消息设置</h2>
|
||||
<Button onClick={() => setIsAddDayPlanOpen(true)}>
|
||||
<PlusOutlined className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultActiveKey="0" items={items} />
|
||||
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button onClick={onPrev}>上一步</Button>
|
||||
<Button type="primary" onClick={onNext}>
|
||||
下一步
|
||||
</Button>
|
||||
<Tabs defaultActiveKey="0" items={items} />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{/* 添加天数计划弹窗 */}
|
||||
<Modal
|
||||
|
||||
164
nkebao/src/pages/scenarios/plan/new/steps/base.module.scss
Normal file
164
nkebao/src/pages/scenarios/plan/new/steps/base.module.scss
Normal file
@@ -0,0 +1,164 @@
|
||||
.basic-container {
|
||||
padding: 12px;
|
||||
}
|
||||
.basic-scene-select {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.basic-scene-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
.basic-scene-btn {
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background: rgba(#1677ff,0.1);
|
||||
color: #1677ff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.basic-scene-btn.selected {
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(22,119,255,0.08);
|
||||
}
|
||||
.basic-label {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.basic-input-block {
|
||||
border: 1px solid #eee;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.basic-tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.basic-tag-item{
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.basic-custom-tag-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.basic-success-tip {
|
||||
display: flex;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.basic-materials {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.basic-materials-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
.basic-material-preview{
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
padding-left: 2px;
|
||||
right: 8px;
|
||||
background:rgba(0,0,0,0.5);
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
cursor:pointer;
|
||||
}
|
||||
.basic-material-card {
|
||||
border: 2px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
min-height: 192px;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.basic-material-card.selected {
|
||||
border: 2px solid #1890ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
.basic-material-img {
|
||||
width: 100px;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0;
|
||||
display: block;
|
||||
}
|
||||
.basic-material-name {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
padding: 4px 0;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
text-align: center;
|
||||
z-index: 3;
|
||||
}
|
||||
.basic-add-material {
|
||||
border: 2px dashed #bbb;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
background: #fafbfc;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 190px;
|
||||
}
|
||||
.basic-order-upload {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.basic-order-upload-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.basic-order-upload-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.basic-order-upload-tip {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.basic-phone-settings {
|
||||
margin: 16px 0;
|
||||
background: #f7f8fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.basic-wechat-group {
|
||||
margin: 16px 0;
|
||||
}
|
||||
.basic-footer-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from "react";
|
||||
import { NavBar } from "antd-mobile";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
const WechatAccountDetail: React.FC = () => {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
backArrow
|
||||
style={{ background: "#fff" }}
|
||||
onBack={() => window.history.back()}
|
||||
>
|
||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
微信号详情
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
|
||||
<h3>微信号详情页面</h3>
|
||||
<p>此页面正在开发中...</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default WechatAccountDetail;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from "react";
|
||||
import { NavBar, Button } from "antd-mobile";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
const WechatAccounts: React.FC = () => {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
微信号管理
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<Button size="small" color="primary">
|
||||
<PlusOutlined />
|
||||
<span style={{ marginLeft: 4, fontSize: 12 }}>添加微信号</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
|
||||
<h3>微信号管理页面</h3>
|
||||
<p>此页面正在开发中...</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default WechatAccounts;
|
||||
26
nkebao/src/pages/wechat-accounts/detail/api.ts
Normal file
26
nkebao/src/pages/wechat-accounts/detail/api.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 获取微信号详情
|
||||
export function getWechatAccountDetail(id: string) {
|
||||
return request("/WechatAccount/detail", { id }, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号summary
|
||||
export function getWechatAccountSummary(id: string) {
|
||||
return request(`/v1/wechats/${id}/summary`, {}, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号好友列表
|
||||
export function getWechatFriends(params: {
|
||||
wechatAccountKeyword: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
friendKeyword?: string;
|
||||
}) {
|
||||
return request("/WechatFriend/friendlistData", params, "POST");
|
||||
}
|
||||
|
||||
// 获取微信好友详情
|
||||
export function getWechatFriendDetail(id: string) {
|
||||
return request("/v1/WechatFriend/detail", { id }, "GET");
|
||||
}
|
||||
720
nkebao/src/pages/wechat-accounts/detail/detail.module.scss
Normal file
720
nkebao/src/pages/wechat-accounts/detail/detail.module.scss
Normal file
@@ -0,0 +1,720 @@
|
||||
.wechat-account-detail-page {
|
||||
padding: 16px;
|
||||
background: linear-gradient(to bottom, #f0f8ff, #ffffff);
|
||||
min-height: 100vh;
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.account-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e8f4fd;
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
|
||||
.avatar-section {
|
||||
position: relative;
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #e8f4fd;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
|
||||
&.status-normal {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
&.status-abnormal {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.nickname {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.action-btn {
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e8f4fd;
|
||||
|
||||
.tabs {
|
||||
.adm-tabs-header {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
|
||||
.adm-tabs-tab {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.adm-tabs-tab-active {
|
||||
color: #1677ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.adm-tabs-tab-line {
|
||||
background: #1677ff;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.adm-tabs-content {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-content {
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.info-card {
|
||||
background: linear-gradient(135deg, #e6f7ff, #f0f8ff);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #bae7ff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
color: #1677ff;
|
||||
padding: 6px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
flex: 1;
|
||||
|
||||
.title-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.title-sub {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-value {
|
||||
text-align: right;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1677ff;
|
||||
|
||||
.value-unit {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weight-card {
|
||||
background: linear-gradient(135deg, #fff7e6, #fff2d9);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #ffd591;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.weight-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.weight-icon {
|
||||
font-size: 16px;
|
||||
color: #fa8c16;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.weight-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.weight-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
&.text-green-600 {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.text-yellow-600 {
|
||||
background: #fffbe6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.text-red-600 {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.score-unit {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weight-description {
|
||||
font-size: 12px;
|
||||
color: #fa8c16;
|
||||
background: #fff7e6;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffd591;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.weight-items {
|
||||
.weight-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.item-label {
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
height: 8px;
|
||||
background: #ffd591;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #fa8c16, #ffa940);
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.item-value {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fa8c16;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.restrictions-card {
|
||||
background: linear-gradient(135deg, #fff2f0, #fff1f0);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #ffccc7;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.restrictions-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.restrictions-icon {
|
||||
font-size: 16px;
|
||||
color: #ff4d4f;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.restrictions-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.restrictions-btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.restrictions-list {
|
||||
.restriction-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #ffccc7;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.restriction-info {
|
||||
flex: 1;
|
||||
|
||||
.restriction-reason {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.restriction-date {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.restriction-level {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
|
||||
&.text-red-600 {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.text-yellow-600 {
|
||||
background: #fffbe6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.text-gray-600 {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.friends-content {
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
padding: 40px 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.friend-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.friend-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.friend-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.friend-remark {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-arrow {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-wechat-id {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.friend-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
|
||||
.friend-tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
padding: 20px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.restrictions-detail {
|
||||
.restriction-detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.restriction-detail-info {
|
||||
flex: 1;
|
||||
|
||||
.restriction-detail-reason {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.restriction-detail-date {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.restriction-detail-level {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
|
||||
&.text-red-600 {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.text-yellow-600 {
|
||||
background: #fffbe6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.text-gray-600 {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-detail {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.error-detail {
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
padding: 40px 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-detail-content {
|
||||
.friend-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.friend-detail-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.friend-detail-info {
|
||||
.friend-detail-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.friend-detail-wechat-id {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.friend-detail-items {
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
|
||||
.detail-tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
940
nkebao/src/pages/wechat-accounts/detail/index.tsx
Normal file
940
nkebao/src/pages/wechat-accounts/detail/index.tsx
Normal file
@@ -0,0 +1,940 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
NavBar,
|
||||
Card,
|
||||
Tabs,
|
||||
Button,
|
||||
SpinLoading,
|
||||
Popup,
|
||||
Toast,
|
||||
Input,
|
||||
Avatar,
|
||||
Tag,
|
||||
} from "antd-mobile";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
UserOutlined,
|
||||
ClockCircleOutlined,
|
||||
MessageOutlined,
|
||||
StarOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
RightOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./detail.module.scss";
|
||||
import {
|
||||
getWechatAccountDetail,
|
||||
getWechatAccountSummary,
|
||||
getWechatFriends,
|
||||
getWechatFriendDetail,
|
||||
} from "./api";
|
||||
|
||||
interface WechatAccountSummary {
|
||||
accountAge: string;
|
||||
activityLevel: {
|
||||
allTimes: number;
|
||||
dayTimes: number;
|
||||
};
|
||||
accountWeight: {
|
||||
scope: number;
|
||||
ageWeight: number;
|
||||
activityWeigth: number;
|
||||
restrictWeight: number;
|
||||
realNameWeight: number;
|
||||
};
|
||||
statistics: {
|
||||
todayAdded: number;
|
||||
addLimit: number;
|
||||
};
|
||||
restrictions: {
|
||||
id: number;
|
||||
level: string;
|
||||
reason: string;
|
||||
date: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Friend {
|
||||
id: string;
|
||||
avatar: string;
|
||||
nickname: string;
|
||||
wechatId: string;
|
||||
remark: string;
|
||||
addTime: string;
|
||||
lastInteraction: string;
|
||||
tags: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}>;
|
||||
region: string;
|
||||
source: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface WechatFriendDetail {
|
||||
id: number;
|
||||
avatar: string;
|
||||
nickname: string;
|
||||
region: string;
|
||||
wechatId: string;
|
||||
addDate: string;
|
||||
tags: string[];
|
||||
memo: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
const WechatAccountDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [accountSummary, setAccountSummary] =
|
||||
useState<WechatAccountSummary | null>(null);
|
||||
const [accountInfo, setAccountInfo] = useState<any>(null);
|
||||
const [showRestrictions, setShowRestrictions] = useState(false);
|
||||
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
|
||||
const [showFriendDetail, setShowFriendDetail] = useState(false);
|
||||
const [selectedFriend, setSelectedFriend] = useState<Friend | null>(null);
|
||||
const [friendDetail, setFriendDetail] = useState<WechatFriendDetail | null>(
|
||||
null
|
||||
);
|
||||
const [isLoadingFriendDetail, setIsLoadingFriendDetail] = useState(false);
|
||||
const [friendDetailError, setFriendDetailError] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadingInfo, setLoadingInfo] = useState(true);
|
||||
const [loadingSummary, setLoadingSummary] = useState(true);
|
||||
|
||||
// 好友列表相关状态
|
||||
const [friends, setFriends] = useState<Friend[]>([]);
|
||||
const [friendsPage, setFriendsPage] = useState(1);
|
||||
const [friendsTotal, setFriendsTotal] = useState(0);
|
||||
const [hasMoreFriends, setHasMoreFriends] = useState(true);
|
||||
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
|
||||
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
|
||||
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
|
||||
const friendsObserver = useRef<IntersectionObserver | null>(null);
|
||||
const friendsLoadingRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// 获取基础信息
|
||||
const fetchAccountInfo = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoadingInfo(true);
|
||||
try {
|
||||
const response = await getWechatAccountDetail(id);
|
||||
if (response && response.data) {
|
||||
setAccountInfo(response.data);
|
||||
} else {
|
||||
Toast.show({
|
||||
content: response?.msg || "获取账号信息失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show({ content: "获取账号信息失败", position: "top" });
|
||||
} finally {
|
||||
setLoadingInfo(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// 获取summary
|
||||
const fetchAccountSummary = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoadingSummary(true);
|
||||
try {
|
||||
const response = await getWechatAccountSummary(id);
|
||||
if (response && response.data) {
|
||||
setAccountSummary(response.data);
|
||||
} else {
|
||||
Toast.show({
|
||||
content: response?.msg || "获取账号概览失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show({ content: "获取账号概览失败", position: "top" });
|
||||
} finally {
|
||||
setLoadingSummary(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// 获取好友列表
|
||||
const fetchFriends = useCallback(
|
||||
async (page: number = 1, isNewSearch: boolean = false) => {
|
||||
if (!id || isFetchingFriends) return;
|
||||
|
||||
try {
|
||||
setIsFetchingFriends(true);
|
||||
setHasFriendLoadError(false);
|
||||
const response = await getWechatFriends({
|
||||
wechatAccountKeyword: id,
|
||||
pageIndex: page,
|
||||
pageSize: 20,
|
||||
friendKeyword: searchQuery,
|
||||
});
|
||||
|
||||
if (response && response.data) {
|
||||
const newFriends = response.data.list.map((friend: any) => ({
|
||||
id: friend.id.toString(),
|
||||
avatar: friend.avatar || "/placeholder.svg",
|
||||
nickname: friend.nickname || "未知用户",
|
||||
wechatId: friend.wechatId || "",
|
||||
remark: friend.memo || "",
|
||||
addTime:
|
||||
friend.createTime || new Date().toISOString().split("T")[0],
|
||||
lastInteraction:
|
||||
friend.lastInteraction || new Date().toISOString().split("T")[0],
|
||||
tags: friend.tags
|
||||
? friend.tags.map((tag: string, index: number) => ({
|
||||
id: `tag-${index}`,
|
||||
name: tag,
|
||||
color: getRandomTagColor(),
|
||||
}))
|
||||
: [],
|
||||
region: friend.region || "未知",
|
||||
source: friend.source || "未知",
|
||||
notes: friend.notes || "",
|
||||
}));
|
||||
|
||||
if (isNewSearch) {
|
||||
setFriends(newFriends);
|
||||
if (newFriends.length === 0) {
|
||||
setIsFriendsEmpty(true);
|
||||
setHasMoreFriends(false);
|
||||
} else {
|
||||
setIsFriendsEmpty(false);
|
||||
setHasMoreFriends(newFriends.length === 20);
|
||||
}
|
||||
} else {
|
||||
setFriends((prev) => [...prev, ...newFriends]);
|
||||
setHasMoreFriends(newFriends.length === 20);
|
||||
}
|
||||
|
||||
setFriendsTotal(response.data.total);
|
||||
setFriendsPage(page);
|
||||
} else {
|
||||
setHasFriendLoadError(true);
|
||||
if (isNewSearch) {
|
||||
setFriends([]);
|
||||
setIsFriendsEmpty(true);
|
||||
setHasMoreFriends(false);
|
||||
}
|
||||
Toast.show({
|
||||
content: response?.msg || "获取好友列表失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取好友列表失败:", error);
|
||||
setHasFriendLoadError(true);
|
||||
if (isNewSearch) {
|
||||
setFriends([]);
|
||||
setIsFriendsEmpty(true);
|
||||
setHasMoreFriends(false);
|
||||
}
|
||||
Toast.show({
|
||||
content: "获取好友列表失败,请检查网络连接",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setIsFetchingFriends(false);
|
||||
}
|
||||
},
|
||||
[id, searchQuery, isFetchingFriends]
|
||||
);
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchAccountInfo();
|
||||
fetchAccountSummary();
|
||||
if (activeTab === "friends") {
|
||||
fetchFriends(1, true);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [id]);
|
||||
|
||||
// 监听标签切换
|
||||
useEffect(() => {
|
||||
if (activeTab === "friends" && id) {
|
||||
setIsFriendsEmpty(false);
|
||||
setHasFriendLoadError(false);
|
||||
fetchFriends(1, true);
|
||||
}
|
||||
}, [activeTab, id, fetchFriends]);
|
||||
|
||||
// 无限滚动加载好友
|
||||
useEffect(() => {
|
||||
if (
|
||||
!friendsLoadingRef.current ||
|
||||
!hasMoreFriends ||
|
||||
isFetchingFriends ||
|
||||
isFriendsEmpty
|
||||
)
|
||||
return;
|
||||
|
||||
friendsObserver.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (
|
||||
entries[0].isIntersecting &&
|
||||
hasMoreFriends &&
|
||||
!isFetchingFriends &&
|
||||
!isFriendsEmpty
|
||||
) {
|
||||
fetchFriends(friendsPage + 1, false);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
friendsObserver.current.observe(friendsLoadingRef.current);
|
||||
|
||||
return () => {
|
||||
if (friendsObserver.current) {
|
||||
friendsObserver.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
hasMoreFriends,
|
||||
isFetchingFriends,
|
||||
friendsPage,
|
||||
fetchFriends,
|
||||
isFriendsEmpty,
|
||||
]);
|
||||
|
||||
// 工具函数
|
||||
const getRandomTagColor = (): string => {
|
||||
const colors = [
|
||||
"bg-blue-100 text-blue-800",
|
||||
"bg-green-100 text-green-800",
|
||||
"bg-red-100 text-red-800",
|
||||
"bg-pink-100 text-pink-800",
|
||||
"bg-emerald-100 text-emerald-800",
|
||||
"bg-amber-100 text-amber-800",
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
const calculateAccountAge = (registerTime: string) => {
|
||||
const registerDate = new Date(registerTime);
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now.getTime() - registerDate.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
const years = Math.floor(diffDays / 365);
|
||||
const months = Math.floor((diffDays % 365) / 30);
|
||||
return { years, months };
|
||||
};
|
||||
|
||||
const formatAccountAge = (age: { years: number; months: number }) => {
|
||||
if (age.years > 0) {
|
||||
return `${age.years}年${age.months}个月`;
|
||||
}
|
||||
return `${age.months}个月`;
|
||||
};
|
||||
|
||||
const getWeightColor = (weight: number) => {
|
||||
if (weight >= 80) return "text-green-600";
|
||||
if (weight >= 60) return "text-yellow-600";
|
||||
return "text-red-600";
|
||||
};
|
||||
|
||||
const getWeightDescription = (weight: number) => {
|
||||
if (weight >= 80) return "账号质量优秀,可以正常使用";
|
||||
if (weight >= 60) return "账号质量良好,需要注意使用频率";
|
||||
return "账号质量较差,建议谨慎使用";
|
||||
};
|
||||
|
||||
const handleTransferFriends = () => {
|
||||
setShowTransferConfirm(true);
|
||||
};
|
||||
|
||||
const confirmTransferFriends = () => {
|
||||
Toast.show({
|
||||
content: "好友转移计划已创建,请在场景获客中查看详情",
|
||||
position: "top",
|
||||
});
|
||||
setShowTransferConfirm(false);
|
||||
navigate("/scenarios");
|
||||
};
|
||||
|
||||
const handleFriendClick = async (friend: Friend) => {
|
||||
setSelectedFriend(friend);
|
||||
setShowFriendDetail(true);
|
||||
setIsLoadingFriendDetail(true);
|
||||
setFriendDetailError(null);
|
||||
|
||||
try {
|
||||
const response = await getWechatFriendDetail(friend.id);
|
||||
if (response && response.data) {
|
||||
setFriendDetail(response.data);
|
||||
} else {
|
||||
setFriendDetailError(response?.msg || "获取好友详情失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取好友详情失败:", error);
|
||||
setFriendDetailError("网络错误,请稍后重试");
|
||||
} finally {
|
||||
setIsLoadingFriendDetail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getRestrictionLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case "high":
|
||||
return "text-red-600";
|
||||
case "medium":
|
||||
return "text-yellow-600";
|
||||
default:
|
||||
return "text-gray-600";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date
|
||||
.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
.replace(/\//g, "-");
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setIsFriendsEmpty(false);
|
||||
setHasFriendLoadError(false);
|
||||
fetchFriends(1, true);
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
};
|
||||
|
||||
if (loadingInfo || loadingSummary) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<span className={style["nav-title"]}>微信号详情</span>
|
||||
</NavBar>
|
||||
}
|
||||
>
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="微信号详情" />}>
|
||||
<div className={style["wechat-account-detail-page"]}>
|
||||
{/* 账号基本信息卡片 */}
|
||||
<Card className={style["account-card"]}>
|
||||
<div className={style["account-info"]}>
|
||||
<div className={style["avatar-section"]}>
|
||||
<Avatar
|
||||
src={accountInfo?.avatar || "/placeholder.svg"}
|
||||
className={style["avatar"]}
|
||||
/>
|
||||
<div
|
||||
className={`${style["status-dot"]} ${accountInfo?.wechatStatus === 1 ? style["status-normal"] : style["status-abnormal"]}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["info-section"]}>
|
||||
<div className={style["name-row"]}>
|
||||
<h2 className={style["nickname"]}>
|
||||
{accountInfo?.nickname || "未知昵称"}
|
||||
</h2>
|
||||
<Tag
|
||||
color={accountInfo?.wechatStatus === 1 ? "success" : "danger"}
|
||||
className={style["status-tag"]}
|
||||
>
|
||||
{accountInfo?.wechatStatus === 1 ? "正常" : "异常"}
|
||||
</Tag>
|
||||
</div>
|
||||
<p className={style["wechat-id"]}>
|
||||
微信号:{accountInfo?.wechatAccount || "未知"}
|
||||
</p>
|
||||
<div className={style["action-buttons"]}>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
className={style["action-btn"]}
|
||||
>
|
||||
<UserOutlined /> {accountInfo?.deviceMemo || "未知设备"}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
className={style["action-btn"]}
|
||||
onClick={handleTransferFriends}
|
||||
>
|
||||
<UserOutlined /> 好友转移
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 标签页 */}
|
||||
<Card className={style["tabs-card"]}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={handleTabChange}
|
||||
className={style["tabs"]}
|
||||
>
|
||||
<Tabs.Tab title="账号概览" key="overview">
|
||||
<div className={style["overview-content"]}>
|
||||
{/* 账号基础信息 */}
|
||||
<div className={style["info-grid"]}>
|
||||
<div className={style["info-card"]}>
|
||||
<div className={style["info-header"]}>
|
||||
<ClockCircleOutlined className={style["info-icon"]} />
|
||||
<div className={style["info-title"]}>
|
||||
<div className={style["title-text"]}>账号年龄</div>
|
||||
{accountSummary && (
|
||||
<div className={style["title-sub"]}>
|
||||
注册于{" "}
|
||||
{new Date(
|
||||
accountSummary.accountAge
|
||||
).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{accountSummary && (
|
||||
<div className={style["info-value"]}>
|
||||
{formatAccountAge(
|
||||
calculateAccountAge(accountSummary.accountAge)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={style["info-card"]}>
|
||||
<div className={style["info-header"]}>
|
||||
<MessageOutlined className={style["info-icon"]} />
|
||||
<div className={style["info-title"]}>
|
||||
<div className={style["title-text"]}>活跃程度</div>
|
||||
{accountSummary && (
|
||||
<div className={style["title-sub"]}>
|
||||
总聊天{" "}
|
||||
{accountSummary.activityLevel.allTimes.toLocaleString()}{" "}
|
||||
次
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{accountSummary && (
|
||||
<div className={style["info-value"]}>
|
||||
{accountSummary.activityLevel.dayTimes.toLocaleString()}
|
||||
<span className={style["value-unit"]}>次/天</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账号权重评估 */}
|
||||
{accountSummary && (
|
||||
<div className={style["weight-card"]}>
|
||||
<div className={style["weight-header"]}>
|
||||
<StarOutlined className={style["weight-icon"]} />
|
||||
<span className={style["weight-title"]}>
|
||||
账号权重评估
|
||||
</span>
|
||||
<div
|
||||
className={`${style["weight-score"]} ${getWeightColor(accountSummary.accountWeight.scope)}`}
|
||||
>
|
||||
<span className={style["score-value"]}>
|
||||
{accountSummary.accountWeight.scope}
|
||||
</span>
|
||||
<span className={style["score-unit"]}>分</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className={style["weight-description"]}>
|
||||
{getWeightDescription(accountSummary.accountWeight.scope)}
|
||||
</p>
|
||||
<div className={style["weight-items"]}>
|
||||
<div className={style["weight-item"]}>
|
||||
<span className={style["item-label"]}>账号年龄</span>
|
||||
<div className={style["progress-bar"]}>
|
||||
<div
|
||||
className={style["progress-fill"]}
|
||||
style={{
|
||||
width: `${accountSummary.accountWeight.ageWeight}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={style["item-value"]}>
|
||||
{accountSummary.accountWeight.ageWeight}%
|
||||
</span>
|
||||
</div>
|
||||
<div className={style["weight-item"]}>
|
||||
<span className={style["item-label"]}>活跃度</span>
|
||||
<div className={style["progress-bar"]}>
|
||||
<div
|
||||
className={style["progress-fill"]}
|
||||
style={{
|
||||
width: `${accountSummary.accountWeight.activityWeigth}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className={style["item-value"]}>
|
||||
{accountSummary.accountWeight.activityWeigth}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 限制记录 */}
|
||||
{accountSummary &&
|
||||
accountSummary.restrictions &&
|
||||
accountSummary.restrictions.length > 0 && (
|
||||
<div className={style["restrictions-card"]}>
|
||||
<div className={style["restrictions-header"]}>
|
||||
<ExclamationCircleOutlined
|
||||
className={style["restrictions-icon"]}
|
||||
/>
|
||||
<span className={style["restrictions-title"]}>
|
||||
限制记录
|
||||
</span>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setShowRestrictions(true)}
|
||||
className={style["restrictions-btn"]}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["restrictions-list"]}>
|
||||
{accountSummary.restrictions
|
||||
.slice(0, 3)
|
||||
.map((restriction) => (
|
||||
<div
|
||||
key={restriction.id}
|
||||
className={style["restriction-item"]}
|
||||
>
|
||||
<div className={style["restriction-info"]}>
|
||||
<span className={style["restriction-reason"]}>
|
||||
{restriction.reason}
|
||||
</span>
|
||||
<span className={style["restriction-date"]}>
|
||||
{formatDateTime(restriction.date)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`${style["restriction-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
||||
>
|
||||
{restriction.level === "high"
|
||||
? "高风险"
|
||||
: restriction.level === "medium"
|
||||
? "中风险"
|
||||
: "低风险"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab
|
||||
title={`好友列表${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
|
||||
key="friends"
|
||||
>
|
||||
<div className={style["friends-content"]}>
|
||||
{/* 搜索栏 */}
|
||||
<div className={style["search-bar"]}>
|
||||
<div className={style["search-input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="搜索好友昵称/微信号"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
size="large"
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleSearch}
|
||||
loading={isFetchingFriends}
|
||||
className={style["search-btn"]}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 好友列表 */}
|
||||
<div className={style["friends-list"]}>
|
||||
{isFriendsEmpty ? (
|
||||
<div className={style["empty"]}>暂无好友数据</div>
|
||||
) : hasFriendLoadError ? (
|
||||
<div className={style["error"]}>
|
||||
<p>加载失败,请重试</p>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => fetchFriends(1, true)}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{friends.map((friend) => (
|
||||
<div
|
||||
key={friend.id}
|
||||
className={style["friend-item"]}
|
||||
onClick={() => handleFriendClick(friend)}
|
||||
>
|
||||
<Avatar
|
||||
src={friend.avatar}
|
||||
className={style["friend-avatar"]}
|
||||
/>
|
||||
<div className={style["friend-info"]}>
|
||||
<div className={style["friend-header"]}>
|
||||
<div className={style["friend-name"]}>
|
||||
{friend.nickname}
|
||||
{friend.remark && (
|
||||
<span className={style["friend-remark"]}>
|
||||
({friend.remark})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<RightOutlined
|
||||
className={style["friend-arrow"]}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["friend-wechat-id"]}>
|
||||
{friend.wechatId}
|
||||
</div>
|
||||
<div className={style["friend-tags"]}>
|
||||
{friend.tags?.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
size="small"
|
||||
className={style["friend-tag"]}
|
||||
>
|
||||
{typeof tag === "string" ? tag : tag.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{hasMoreFriends && !isFriendsEmpty && (
|
||||
<div
|
||||
ref={friendsLoadingRef}
|
||||
className={style["loading-more"]}
|
||||
>
|
||||
<SpinLoading
|
||||
color="primary"
|
||||
style={{ fontSize: 24 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 限制记录详情弹窗 */}
|
||||
<Popup
|
||||
visible={showRestrictions}
|
||||
onMaskClick={() => setShowRestrictions(false)}
|
||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||
>
|
||||
<div className={style["popup-content"]}>
|
||||
<div className={style["popup-header"]}>
|
||||
<h3>限制记录详情</h3>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setShowRestrictions(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<p className={style["popup-description"]}>每次限制恢复时间为24小时</p>
|
||||
{accountSummary && accountSummary.restrictions && (
|
||||
<div className={style["restrictions-detail"]}>
|
||||
{accountSummary.restrictions.map((restriction) => (
|
||||
<div
|
||||
key={restriction.id}
|
||||
className={style["restriction-detail-item"]}
|
||||
>
|
||||
<div className={style["restriction-detail-info"]}>
|
||||
<div className={style["restriction-detail-reason"]}>
|
||||
{restriction.reason}
|
||||
</div>
|
||||
<div className={style["restriction-detail-date"]}>
|
||||
{formatDateTime(restriction.date)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`${style["restriction-detail-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
||||
>
|
||||
{restriction.level === "high"
|
||||
? "高风险"
|
||||
: restriction.level === "medium"
|
||||
? "中风险"
|
||||
: "低风险"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 好友转移确认弹窗 */}
|
||||
<Popup
|
||||
visible={showTransferConfirm}
|
||||
onMaskClick={() => setShowTransferConfirm(false)}
|
||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||
>
|
||||
<div className={style["popup-content"]}>
|
||||
<div className={style["popup-header"]}>
|
||||
<h3>确认好友转移</h3>
|
||||
</div>
|
||||
<p className={style["popup-description"]}>
|
||||
确定要将该微信号的好友转移到其他账号吗?此操作将创建一个好友转移计划。
|
||||
</p>
|
||||
<div className={style["popup-actions"]}>
|
||||
<Button block color="primary" onClick={confirmTransferFriends}>
|
||||
确认转移
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
color="danger"
|
||||
fill="outline"
|
||||
onClick={() => setShowTransferConfirm(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 好友详情弹窗 */}
|
||||
<Popup
|
||||
visible={showFriendDetail}
|
||||
onMaskClick={() => setShowFriendDetail(false)}
|
||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||
>
|
||||
<div className={style["popup-content"]}>
|
||||
<div className={style["popup-header"]}>
|
||||
<h3>好友详情</h3>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setShowFriendDetail(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoadingFriendDetail ? (
|
||||
<div className={style["loading-detail"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
</div>
|
||||
) : friendDetailError ? (
|
||||
<div className={style["error-detail"]}>
|
||||
<p>{friendDetailError}</p>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleFriendClick(selectedFriend!)}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
) : friendDetail && selectedFriend ? (
|
||||
<div className={style["friend-detail-content"]}>
|
||||
<div className={style["friend-detail-header"]}>
|
||||
<Avatar
|
||||
src={selectedFriend.avatar}
|
||||
className={style["friend-detail-avatar"]}
|
||||
/>
|
||||
<div className={style["friend-detail-info"]}>
|
||||
<h4 className={style["friend-detail-name"]}>
|
||||
{selectedFriend.nickname}
|
||||
</h4>
|
||||
<p className={style["friend-detail-wechat-id"]}>
|
||||
微信号:{selectedFriend.wechatId}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style["friend-detail-items"]}>
|
||||
<div className={style["detail-item"]}>
|
||||
<span className={style["detail-label"]}>地区</span>
|
||||
<span className={style["detail-value"]}>
|
||||
{friendDetail.region || "未知"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style["detail-item"]}>
|
||||
<span className={style["detail-label"]}>添加时间</span>
|
||||
<span className={style["detail-value"]}>
|
||||
{friendDetail.addDate}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style["detail-item"]}>
|
||||
<span className={style["detail-label"]}>来源</span>
|
||||
<span className={style["detail-value"]}>
|
||||
{friendDetail.source || "未知"}
|
||||
</span>
|
||||
</div>
|
||||
{friendDetail.memo && (
|
||||
<div className={style["detail-item"]}>
|
||||
<span className={style["detail-label"]}>备注</span>
|
||||
<span className={style["detail-value"]}>
|
||||
{friendDetail.memo}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{friendDetail.tags && friendDetail.tags.length > 0 && (
|
||||
<div className={style["detail-item"]}>
|
||||
<span className={style["detail-label"]}>标签</span>
|
||||
<div className={style["detail-tags"]}>
|
||||
{friendDetail.tags.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
size="small"
|
||||
className={style["detail-tag"]}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Popup>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default WechatAccountDetail;
|
||||
30
nkebao/src/pages/wechat-accounts/list/api.ts
Normal file
30
nkebao/src/pages/wechat-accounts/list/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 获取微信号列表
|
||||
export function getWechatAccounts(params: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
keyword?: string;
|
||||
}) {
|
||||
return request("v1/wechats", params, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号详情
|
||||
export function getWechatAccountDetail(id: string) {
|
||||
return request("v1/WechatAccount/detail", { id }, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号好友列表
|
||||
export function getWechatFriends(params: {
|
||||
wechatAccountKeyword: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
friendKeyword?: string;
|
||||
}) {
|
||||
return request("v1/WechatFriend/friendlistData", params, "POST");
|
||||
}
|
||||
|
||||
// 获取微信好友详情
|
||||
export function getWechatFriendDetail(id: string) {
|
||||
return request("v1/WechatFriend/detail", { id }, "GET");
|
||||
}
|
||||
171
nkebao/src/pages/wechat-accounts/list/index.module.scss
Normal file
171
nkebao/src/pages/wechat-accounts/list/index.module.scss
Normal file
@@ -0,0 +1,171 @@
|
||||
.wechat-accounts-page {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.account-card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 14px 14px 10px 14px;
|
||||
transition: box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
border: 1px solid #f0f0f0;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.10);
|
||||
border-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #e6f0fa;
|
||||
box-shadow: 0 0 0 2px #1677ff33;
|
||||
object-fit: cover;
|
||||
}
|
||||
.status-dot-normal {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #52c41a;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.status-dot-abnormal {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #ff4d4f;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.header-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.nickname-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.nickname {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-label-normal {
|
||||
background: #e6fffb;
|
||||
color: #13c2c2;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 2px 8px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.status-label-abnormal {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 2px 8px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.wechat-id {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.card-action {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.card-body {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.row-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
.row-item {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.strong {
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
.strong-green {
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
.progress-bar {
|
||||
margin: 6px 0 8px 0;
|
||||
}
|
||||
.progress-bg {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 8px;
|
||||
background: linear-gradient(90deg, #1677ff 0%, #69c0ff 100%);
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.pagination {
|
||||
margin: 16px 0 0 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.popup-content {
|
||||
padding: 16px 0 8px 0;
|
||||
}
|
||||
.popup-content img {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 48px 0 32px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
309
nkebao/src/pages/wechat-accounts/list/index.tsx
Normal file
309
nkebao/src/pages/wechat-accounts/list/index.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
NavBar,
|
||||
List,
|
||||
Card,
|
||||
Button,
|
||||
SpinLoading,
|
||||
Popup,
|
||||
Toast,
|
||||
} from "antd-mobile";
|
||||
import { Pagination, Input, Tooltip } from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
import { getWechatAccounts } from "./api";
|
||||
|
||||
interface WechatAccount {
|
||||
id: number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
wechatId: string;
|
||||
wechatAccount: string;
|
||||
deviceId: number;
|
||||
times: number; // 今日可添加
|
||||
addedCount: number; // 今日新增
|
||||
wechatStatus: number; // 1正常 0异常
|
||||
totalFriend: number;
|
||||
deviceMemo: string; // 设备名
|
||||
activeTime: string; // 最后活跃
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const WechatAccounts: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalAccounts, setTotalAccounts] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const fetchAccounts = async (page = 1, keyword = "") => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getWechatAccounts({
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
keyword,
|
||||
});
|
||||
if (res && res.list) {
|
||||
setAccounts(res.list);
|
||||
setTotalAccounts(res.total || 0);
|
||||
} else {
|
||||
setAccounts([]);
|
||||
setTotalAccounts(0);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show({ content: "获取微信号失败", position: "top" });
|
||||
setAccounts([]);
|
||||
setTotalAccounts(0);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts(currentPage, searchTerm);
|
||||
// eslint-disable-next-line
|
||||
}, [currentPage]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
fetchAccounts(1, searchTerm);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await fetchAccounts(currentPage, searchTerm);
|
||||
setIsRefreshing(false);
|
||||
Toast.show({ content: "刷新成功", position: "top" });
|
||||
};
|
||||
|
||||
const handleAccountClick = (account: WechatAccount) => {
|
||||
setSelectedAccount(account);
|
||||
setPopupVisible(true);
|
||||
};
|
||||
|
||||
const handleTransferFriends = (account: WechatAccount) => {
|
||||
// TODO: 实现好友转移弹窗或跳转
|
||||
Toast.show({ content: `好友转移:${account.nickname}` });
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div className={style["nav-title"]}>
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className={style["nav-title"]}>微信号管理</span>
|
||||
</NavBar>
|
||||
<div className="search-bar">
|
||||
<div className="search-input-wrapper">
|
||||
<Input
|
||||
placeholder="搜索微信号/昵称"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
size="large"
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
loading={isRefreshing}
|
||||
className="refresh-btn"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={style["wechat-accounts-page"]}>
|
||||
{isLoading ? (
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
</div>
|
||||
) : accounts.length === 0 ? (
|
||||
<div className={style["empty"]}>暂无微信账号数据</div>
|
||||
) : (
|
||||
<div className={style["card-list"]}>
|
||||
{accounts.map((account) => {
|
||||
const percent =
|
||||
account.times > 0
|
||||
? Math.min((account.addedCount / account.times) * 100, 100)
|
||||
: 0;
|
||||
return (
|
||||
<div
|
||||
key={account.id}
|
||||
className={style["account-card"]}
|
||||
onClick={() => handleAccountClick(account)}
|
||||
>
|
||||
<div className={style["card-header"]}>
|
||||
<div className={style["avatar-wrapper"]}>
|
||||
<img
|
||||
src={account.avatar}
|
||||
alt={account.nickname}
|
||||
className={style["avatar"]}
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
account.wechatStatus === 1
|
||||
? style["status-dot-normal"]
|
||||
: style["status-dot-abnormal"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["header-info"]}>
|
||||
<div className={style["nickname-row"]}>
|
||||
<span className={style["nickname"]}>
|
||||
{account.nickname}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
account.wechatStatus === 1
|
||||
? style["status-label-normal"]
|
||||
: style["status-label-abnormal"]
|
||||
}
|
||||
>
|
||||
{account.wechatStatus === 1 ? "正常" : "异常"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style["wechat-id"]}>
|
||||
微信号:{account.wechatAccount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["card-body"]}>
|
||||
<div className={style["row-group"]}>
|
||||
<div className={style["row-item"]}>
|
||||
<span>好友数量:</span>
|
||||
<span className={style["strong"]}>
|
||||
{account.totalFriend}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style["row-item"]}>
|
||||
<span>今日新增:</span>
|
||||
<span className={style["strong-green"]}>
|
||||
+{account.addedCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["row-group"]}>
|
||||
<div className={style["row-item"]}>
|
||||
<span>今日可添加:</span>
|
||||
<span>{account.times}</span>
|
||||
</div>
|
||||
<div className={style["row-item"]}>
|
||||
<Tooltip title={`每日最多添加 ${account.times} 个好友`}>
|
||||
<span>进度:</span>
|
||||
<span>
|
||||
{account.addedCount}/{account.times}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["progress-bar"]}>
|
||||
<div className={style["progress-bg"]}>
|
||||
<div
|
||||
className={style["progress-fill"]}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["row-group"]}>
|
||||
<div className={style["row-item"]}>
|
||||
<span>所属设备:</span>
|
||||
<span>{account.deviceMemo || "-"}</span>
|
||||
</div>
|
||||
<div className={style["row-item"]}>
|
||||
<span>最后活跃:</span>
|
||||
<span>{account.activeTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={style["pagination"]}>
|
||||
{totalAccounts > PAGE_SIZE && (
|
||||
<Pagination
|
||||
total={Math.ceil(totalAccounts / PAGE_SIZE)}
|
||||
current={currentPage}
|
||||
onChange={setCurrentPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Popup
|
||||
visible={popupVisible}
|
||||
onMaskClick={() => setPopupVisible(false)}
|
||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||
>
|
||||
{selectedAccount && (
|
||||
<div className={style["popup-content"]}>
|
||||
<div style={{ textAlign: "center", margin: 16 }}>
|
||||
<img
|
||||
src={selectedAccount.avatar}
|
||||
alt="avatar"
|
||||
style={{ width: 60, height: 60, borderRadius: 30 }}
|
||||
/>
|
||||
<div style={{ fontWeight: 600, marginTop: 8 }}>
|
||||
{selectedAccount.nickname}
|
||||
</div>
|
||||
<div style={{ color: "#888", fontSize: 12 }}>
|
||||
微信号:{selectedAccount.wechatAccount}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ margin: 16 }}>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate(`/wechat-accounts/detail/${selectedAccount.id}`);
|
||||
}}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
color="danger"
|
||||
fill="outline"
|
||||
onClick={() => setPopupVisible(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Popup>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default WechatAccounts;
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
PlusOutlined,
|
||||
MinusOutlined,
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Switch, Spin, message } from "antd";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import {
|
||||
createAutoLikeTask,
|
||||
updateAutoLikeTask,
|
||||
@@ -29,9 +26,9 @@ const contentTypeLabels: Record<ContentType, string> = {
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{ title: "基础设置", desc: "设置点赞规则" },
|
||||
{ title: "设备选择", desc: "选择执行设备" },
|
||||
{ title: "人群选择", desc: "选择目标人群" },
|
||||
{ id: 1, title: "基础设置", subtitle: "设置点赞规则" },
|
||||
{ id: 2, title: "设备选择", subtitle: "选择执行设备" },
|
||||
{ id: 3, title: "人群选择", subtitle: "选择目标人群" },
|
||||
];
|
||||
|
||||
const NewAutoLike: React.FC = () => {
|
||||
@@ -146,38 +143,7 @@ const NewAutoLike: React.FC = () => {
|
||||
|
||||
// 步骤器
|
||||
const renderStepIndicator = () => (
|
||||
<div className={style.stepIndicatorWrapper}>
|
||||
<div className={style.stepIndicator}>
|
||||
{steps.map((s, i) => (
|
||||
<div
|
||||
key={s.title}
|
||||
className={
|
||||
style.stepItem +
|
||||
" " +
|
||||
(currentStep === i + 1
|
||||
? style.stepActive
|
||||
: i + 1 < currentStep
|
||||
? style.stepDone
|
||||
: "")
|
||||
}
|
||||
>
|
||||
<span className={style.stepNum}>
|
||||
{i + 1 < currentStep ? <CheckOutlined /> : i + 1}
|
||||
</span>
|
||||
<span className={style.stepTitle}>{s.title}</span>
|
||||
<span className={style.stepDesc}>{s.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={style.stepProgressBarBg}>
|
||||
<div
|
||||
className={style.stepProgressBar}
|
||||
style={{
|
||||
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<StepIndicator steps={steps} currentStep={currentStep} />
|
||||
);
|
||||
|
||||
// 步骤1:基础设置
|
||||
@@ -217,7 +183,6 @@ const NewAutoLike: React.FC = () => {
|
||||
}
|
||||
className={style.counterInput}
|
||||
/>
|
||||
<span className={style.counterUnit}>秒</span>
|
||||
</div>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
@@ -226,6 +191,7 @@ const NewAutoLike: React.FC = () => {
|
||||
}
|
||||
className={style.counterBtn}
|
||||
/>
|
||||
<span className={style.counterUnit}>秒</span>
|
||||
</div>
|
||||
<div className={style.counterTip}>设置两次点赞之间的最小时间间隔</div>
|
||||
</div>
|
||||
@@ -254,7 +220,6 @@ const NewAutoLike: React.FC = () => {
|
||||
}
|
||||
className={style.counterInput}
|
||||
/>
|
||||
<span className={style.counterUnit}>次/天</span>
|
||||
</div>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
@@ -263,6 +228,7 @@ const NewAutoLike: React.FC = () => {
|
||||
}
|
||||
className={style.counterBtn}
|
||||
/>
|
||||
<span className={style.counterUnit}>次/天</span>
|
||||
</div>
|
||||
<div className={style.counterTip}>设置每天最多点赞的次数</div>
|
||||
</div>
|
||||
@@ -366,7 +332,6 @@ const NewAutoLike: React.FC = () => {
|
||||
<DeviceSelection
|
||||
selectedDevices={formData.devices}
|
||||
onSelect={(devices) => handleUpdateFormData({ devices })}
|
||||
mode="dialog"
|
||||
showInput={true}
|
||||
showSelectedList={true}
|
||||
/>
|
||||
@@ -396,7 +361,7 @@ const NewAutoLike: React.FC = () => {
|
||||
<div className={style.basicSection}>
|
||||
<div className={style.formItem}>
|
||||
<FriendSelection
|
||||
selectedFriends={formData.friends}
|
||||
selectedFriends={formData.friends || []}
|
||||
onSelect={(friends) => handleUpdateFormData({ friends })}
|
||||
deviceIds={formData.devices}
|
||||
/>
|
||||
@@ -415,7 +380,7 @@ const NewAutoLike: React.FC = () => {
|
||||
className={style.completeBtn}
|
||||
size="large"
|
||||
loading={isSubmitting}
|
||||
disabled={formData.friends.length === 0}
|
||||
disabled={!formData.friends || formData.friends.length === 0}
|
||||
>
|
||||
{isEditMode ? "更新任务" : "创建任务"}
|
||||
</Button>
|
||||
@@ -426,21 +391,10 @@ const NewAutoLike: React.FC = () => {
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<div className={style.headerBar}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate(-1)}
|
||||
className={style.backBtn}
|
||||
/>
|
||||
<span className={style.title}>
|
||||
{isEditMode ? "编辑自动点赞" : "新建自动点赞"}
|
||||
</span>
|
||||
</div>
|
||||
<NavCommon title={isEditMode ? "编辑自动点赞" : "新建自动点赞"} />
|
||||
{renderStepIndicator()}
|
||||
</>
|
||||
}
|
||||
footer={<MeauMobile activeKey="workspace" />}
|
||||
>
|
||||
<div className={style.formBg}>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -5,95 +5,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stepIndicatorWrapper {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: #f8f6f3;
|
||||
padding: 16px 0 8px 0;
|
||||
}
|
||||
|
||||
.stepIndicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.stepItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #bbb;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
transition: color 0.2s;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.stepActive {
|
||||
color: #188eee;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stepDone {
|
||||
color: #19c37d;
|
||||
}
|
||||
|
||||
.stepNum {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
color: #888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stepActive .stepNum {
|
||||
background: #188eee;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stepDone .stepNum {
|
||||
background: #19c37d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stepTitle {
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stepDesc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stepProgressBarBg {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
margin: 12px auto 0 auto;
|
||||
}
|
||||
|
||||
.stepProgressBar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: #188eee;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.basicSection {
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
@@ -127,7 +38,7 @@
|
||||
}
|
||||
|
||||
.inputTime {
|
||||
width: 90px;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
@@ -193,11 +104,9 @@
|
||||
}
|
||||
|
||||
.counterUnit {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.timeSeparator {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Home from "@/pages/home/index";
|
||||
import Mine from "@/pages/mine/index";
|
||||
import WechatAccounts from "@/pages/wechat-accounts/list/index";
|
||||
import WechatAccountDetail from "@/pages/wechat-accounts/detail/index";
|
||||
|
||||
const routes = [
|
||||
// 基础路由
|
||||
@@ -13,6 +15,17 @@ const routes = [
|
||||
element: <Mine />,
|
||||
auth: true,
|
||||
},
|
||||
// 微信号管理路由
|
||||
{
|
||||
path: "/wechat-accounts",
|
||||
element: <WechatAccounts />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/wechat-accounts/detail/:id",
|
||||
element: <WechatAccountDetail />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import WechatAccounts from "@/pages/wechat-accounts/WechatAccounts";
|
||||
import WechatAccountDetail from "@/pages/wechat-accounts/WechatAccountDetail";
|
||||
|
||||
const wechatAccountRoutes = [
|
||||
{
|
||||
path: "/wechat-accounts",
|
||||
element: <WechatAccounts />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/wechat-accounts/:id",
|
||||
element: <WechatAccountDetail />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default wechatAccountRoutes;
|
||||
import WechatAccounts from "@/pages/wechat-accounts/list";
|
||||
import WechatAccountDetail from "@/pages/wechat-accounts/detail";
|
||||
|
||||
const wechatAccountRoutes = [
|
||||
{
|
||||
path: "/wechat-accounts",
|
||||
element: <WechatAccounts />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/wechat-accounts/detail/:id",
|
||||
element: <WechatAccountDetail />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default wechatAccountRoutes;
|
||||
|
||||
@@ -1,166 +1,166 @@
|
||||
/* Reset & 基础样式,兼容移动端和PC端,补充 iOS/安卓容差处理 */
|
||||
|
||||
/* 主题色变量定义 */
|
||||
:root {
|
||||
--primary-color: #188eee;
|
||||
--primary-color-light: #40a9ff;
|
||||
--primary-color-dark: #096dd9;
|
||||
--primary-gradient: linear-gradient(135deg, #188eee 0%, #096dd9 100%);
|
||||
--primary-shadow: rgba(24, 142, 238, 0.3);
|
||||
--primary-shadow-light: rgba(24, 142, 238, 0.1);
|
||||
--primary-shadow-dark: rgba(24, 142, 238, 0.4);
|
||||
}
|
||||
|
||||
/* 1. 通用 Reset */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center, dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend, table, caption,
|
||||
tbody, tfoot, thead, tr, th, td, article,
|
||||
aside, canvas, details, embed, figure,
|
||||
figcaption, footer, header, hgroup, menu,
|
||||
nav, output, ruby, section, summary, time,
|
||||
mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 2. HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 3. 列表、表格、图片等 */
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* 4. 链接、按钮等 */
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
button, input, select, textarea {
|
||||
font: inherit;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* 5. 滚动条美化(可选) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 6. 移动端/PC端兼容基础样式 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden; // 禁止 body 滚动和回弹
|
||||
-webkit-tap-highlight-color: transparent; // 移动端点击无高亮
|
||||
-webkit-text-size-adjust: 100%; // 禁止iOS自动调整字体
|
||||
text-size-adjust: 100%;
|
||||
/* iOS/安卓容差补充 */
|
||||
/* 解决 iOS 12+ 及部分安卓机型底部安全区适配 */
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
/* 解决 iOS 顶部刘海屏安全区 */
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
#root, .app-content {
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch; // iOS 惯性滚动
|
||||
/* 安卓部分 WebView 兼容 */
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #222;
|
||||
line-height: 1.5;
|
||||
/* 安卓部分机型字体抗锯齿 */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* iOS input、textarea 默认圆角和阴影去除 */
|
||||
input, textarea {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 安卓部分 WebView 点击延迟优化 */
|
||||
body, input, textarea, select, button {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
// 导航栏样式
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
// 搜索相关样式
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.ant-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.new-task-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
/* Reset & 基础样式,兼容移动端和PC端,补充 iOS/安卓容差处理 */
|
||||
|
||||
/* 主题色变量定义 */
|
||||
:root {
|
||||
--primary-color: #188eee;
|
||||
--primary-color-light: #40a9ff;
|
||||
--primary-color-dark: #096dd9;
|
||||
--primary-gradient: linear-gradient(135deg, #188eee 0%, #096dd9 100%);
|
||||
--primary-shadow: rgba(24, 142, 238, 0.3);
|
||||
--primary-shadow-light: rgba(24, 142, 238, 0.1);
|
||||
--primary-shadow-dark: rgba(24, 142, 238, 0.4);
|
||||
}
|
||||
|
||||
/* 1. 通用 Reset */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center, dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend, table, caption,
|
||||
tbody, tfoot, thead, tr, th, td, article,
|
||||
aside, canvas, details, embed, figure,
|
||||
figcaption, footer, header, hgroup, menu,
|
||||
nav, output, ruby, section, summary, time,
|
||||
mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 2. HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 3. 列表、表格、图片等 */
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* 4. 链接、按钮等 */
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
button, input, select, textarea {
|
||||
font: inherit;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* 5. 滚动条美化(可选) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 6. 移动端/PC端兼容基础样式 */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden; // 禁止 body 滚动和回弹
|
||||
-webkit-tap-highlight-color: transparent; // 移动端点击无高亮
|
||||
-webkit-text-size-adjust: 100%; // 禁止iOS自动调整字体
|
||||
text-size-adjust: 100%;
|
||||
/* iOS/安卓容差补充 */
|
||||
/* 解决 iOS 12+ 及部分安卓机型底部安全区适配 */
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
/* 解决 iOS 顶部刘海屏安全区 */
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
#root, .app-content {
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch; // iOS 惯性滚动
|
||||
/* 安卓部分 WebView 兼容 */
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #222;
|
||||
line-height: 1.5;
|
||||
/* 安卓部分机型字体抗锯齿 */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* iOS input、textarea 默认圆角和阴影去除 */
|
||||
input, textarea {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 安卓部分 WebView 点击延迟优化 */
|
||||
body, input, textarea, select, button {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
// 导航栏样式
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
// 搜索相关样式
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.ant-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.new-task-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user