Merge branch 'yongpxu-dev' into develop
This commit is contained in:
@@ -1,21 +1,30 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Bell, Smartphone, Users, Activity, MessageSquare, TrendingUp } from 'lucide-react';
|
||||
import Chart from 'chart.js/auto';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import UnifiedHeader, { HeaderPresets } from '@/components/UnifiedHeader';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import '@/components/Layout.css';
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Bell,
|
||||
Smartphone,
|
||||
Users,
|
||||
Activity,
|
||||
MessageSquare,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import Chart from "chart.js/auto";
|
||||
import Layout from "@/components/Layout";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
import UnifiedHeader, { HeaderPresets } from "@/components/UnifiedHeader";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import "@/components/Layout.css";
|
||||
|
||||
// API接口定义
|
||||
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "https://ckbapi.quwanzhi.com";
|
||||
const API_BASE_URL =
|
||||
process.env.REACT_APP_API_BASE_URL || "https://ckbapi.quwanzhi.com";
|
||||
|
||||
// 统一的API请求客户端
|
||||
async function apiRequest<T>(url: string): Promise<T> {
|
||||
try {
|
||||
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
||||
const token =
|
||||
typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
@@ -99,7 +108,7 @@ export default function Home() {
|
||||
growth: 12,
|
||||
},
|
||||
{
|
||||
id: "xiaohongshu",
|
||||
id: "xiaohongshu",
|
||||
name: "小红书获客",
|
||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||||
color: "bg-red-100 text-red-600",
|
||||
@@ -135,7 +144,7 @@ export default function Home() {
|
||||
},
|
||||
{
|
||||
title: "群发任务",
|
||||
value: "8",
|
||||
value: "8",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
color: "text-orange-600",
|
||||
path: "/workspace/group-push",
|
||||
@@ -180,10 +189,11 @@ export default function Home() {
|
||||
// 尝试请求API数据
|
||||
try {
|
||||
// 并行请求多个接口
|
||||
const [deviceStatsResult, wechatStatsResult] = await Promise.allSettled([
|
||||
apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`),
|
||||
apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`),
|
||||
]);
|
||||
const [deviceStatsResult, wechatStatsResult] =
|
||||
await Promise.allSettled([
|
||||
apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`),
|
||||
apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`),
|
||||
]);
|
||||
|
||||
const newStats = {
|
||||
totalDevices: 0,
|
||||
@@ -213,7 +223,9 @@ export default function Home() {
|
||||
setStats(newStats);
|
||||
} catch (apiError) {
|
||||
console.warn("API请求失败,使用默认数据:", apiError);
|
||||
setApiError(apiError instanceof Error ? apiError.message : "API连接失败");
|
||||
setApiError(
|
||||
apiError instanceof Error ? apiError.message : "API连接失败"
|
||||
);
|
||||
|
||||
// 使用默认数据
|
||||
setStats({
|
||||
@@ -247,11 +259,11 @@ export default function Home() {
|
||||
}, []); // 移除stats依赖
|
||||
|
||||
const handleDevicesClick = () => {
|
||||
navigate('/profile/devices');
|
||||
navigate("/profile/devices");
|
||||
};
|
||||
|
||||
const handleWechatClick = () => {
|
||||
navigate('/wechat-accounts');
|
||||
navigate("/wechat-accounts");
|
||||
};
|
||||
|
||||
// 使用Chart.js创建图表
|
||||
@@ -263,7 +275,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
const ctx = chartRef.current.getContext("2d");
|
||||
|
||||
|
||||
// 添加null检查
|
||||
if (!ctx) return;
|
||||
|
||||
@@ -391,9 +403,12 @@ export default function Home() {
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">设备数量</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-blue-600">{stats.totalDevices}</span>
|
||||
<span className="text-lg font-bold text-blue-600">
|
||||
{stats.totalDevices}
|
||||
</span>
|
||||
<Smartphone className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div className="h-2"></div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -402,22 +417,31 @@ export default function Home() {
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">微信号数量</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg font-bold text-blue-600">{stats.totalWechatAccounts}</span>
|
||||
<span className="text-lg font-bold text-blue-600">
|
||||
{stats.totalWechatAccounts}
|
||||
</span>
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2"></div>
|
||||
</Card>
|
||||
</div>
|
||||
<Card className="p-3 bg-white">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">在线微信号</span>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-lg font-bold text-blue-600">{stats.onlineWechatAccounts}</span>
|
||||
<span className="text-lg font-bold text-blue-600">
|
||||
{stats.onlineWechatAccounts}
|
||||
</span>
|
||||
<Activity className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
stats.totalWechatAccounts > 0 ? (stats.onlineWechatAccounts / stats.totalWechatAccounts) * 100 : 0
|
||||
stats.totalWechatAccounts > 0
|
||||
? (stats.onlineWechatAccounts /
|
||||
stats.totalWechatAccounts) *
|
||||
100
|
||||
: 0
|
||||
}
|
||||
className="h-1"
|
||||
/>
|
||||
@@ -435,16 +459,30 @@ export default function Home() {
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 4) // 只显示前4个
|
||||
.map((scenario) => (
|
||||
<div
|
||||
<div
|
||||
key={scenario.id}
|
||||
className="block flex-1 cursor-pointer"
|
||||
onClick={() => navigate(`/scenarios/${scenario.id}?name=${encodeURIComponent(scenario.name)}`)}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/scenarios/${scenario.id}?name=${encodeURIComponent(
|
||||
scenario.name
|
||||
)}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center space-y-1">
|
||||
<div className={`w-10 h-10 rounded-full ${scenario.color} flex items-center justify-center`}>
|
||||
<img src={scenario.icon || "/placeholder.svg"} alt={scenario.name} className="w-5 h-5" />
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full ${scenario.color} flex items-center justify-center`}
|
||||
>
|
||||
<img
|
||||
src={scenario.icon || "/placeholder.svg"}
|
||||
alt={scenario.name}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{scenario.value}
|
||||
</div>
|
||||
<div className="text-sm font-medium">{scenario.value}</div>
|
||||
<div className="text-xs text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis w-full">
|
||||
{scenario.name}
|
||||
</div>
|
||||
@@ -466,7 +504,9 @@ export default function Home() {
|
||||
className="flex items-center space-x-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
onClick={() => stat.path && navigate(stat.path)}
|
||||
>
|
||||
<div className={`p-2 rounded-full bg-white ${stat.color}`}>{stat.icon}</div>
|
||||
<div className={`p-2 rounded-full bg-white ${stat.color}`}>
|
||||
{stat.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold">{stat.value}</div>
|
||||
<div className="text-xs text-gray-500">{stat.title}</div>
|
||||
@@ -487,4 +527,4 @@ export default function Home() {
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import {
|
||||
fetchMomentsSyncTaskDetail,
|
||||
toggleMomentsSyncTask,
|
||||
syncMoments
|
||||
} from '@/api/momentsSync';
|
||||
import { MomentsSyncTask } from '@/types/moments-sync';
|
||||
import { ChevronLeft, Edit2, RefreshCw, Clock, Database, Smartphone } from 'lucide-react';
|
||||
import Layout from '@/components/Layout';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import {
|
||||
fetchMomentsSyncTaskDetail,
|
||||
toggleMomentsSyncTask,
|
||||
syncMoments,
|
||||
} from "@/api/momentsSync";
|
||||
import { MomentsSyncTask } from "@/types/moments-sync";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Edit2,
|
||||
RefreshCw,
|
||||
Clock,
|
||||
Database,
|
||||
Smartphone,
|
||||
} from "lucide-react";
|
||||
import Layout from "@/components/Layout";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
|
||||
export default function MomentsSyncDetail() {
|
||||
const { id } = useParams();
|
||||
@@ -31,7 +38,7 @@ export default function MomentsSyncDetail() {
|
||||
setTask(taskData);
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: '获取任务详情失败', variant: 'destructive' });
|
||||
toast({ title: "获取任务详情失败", variant: "destructive" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -49,9 +56,9 @@ export default function MomentsSyncDetail() {
|
||||
const newStatus = task.status === 1 ? 2 : 1;
|
||||
await toggleMomentsSyncTask(id, newStatus.toString());
|
||||
setTask({ ...task, status: newStatus });
|
||||
toast({ title: newStatus === 1 ? '任务已开启' : '任务已暂停' });
|
||||
toast({ title: newStatus === 1 ? "任务已开启" : "任务已暂停" });
|
||||
} catch (error) {
|
||||
toast({ title: '操作失败', variant: 'destructive' });
|
||||
toast({ title: "操作失败", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,10 +66,10 @@ export default function MomentsSyncDetail() {
|
||||
if (!id) return;
|
||||
try {
|
||||
await syncMoments(id);
|
||||
toast({ title: '同步任务已启动' });
|
||||
toast({ title: "同步任务已启动" });
|
||||
fetchTaskDetail(); // 刷新任务详情
|
||||
} catch (error) {
|
||||
toast({ title: '同步失败', variant: 'destructive' });
|
||||
toast({ title: "同步失败", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,7 +98,10 @@ export default function MomentsSyncDetail() {
|
||||
<div className="flex-1 bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-600">任务不存在</p>
|
||||
<Button onClick={() => navigate('/workspace/moments-sync')} className="mt-4">
|
||||
<Button
|
||||
onClick={() => navigate("/workspace/moments-sync")}
|
||||
className="mt-4"
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
@@ -106,25 +116,28 @@ export default function MomentsSyncDetail() {
|
||||
<div className="bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate('/workspace/moments-sync')}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate("/workspace/moments-sync")}
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">朋友圈同步任务详情</h1>
|
||||
<h1 className="text-lg font-medium">任务详情</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" onClick={handleEdit}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleSync}>
|
||||
{/* <Button variant="outline" size="sm" onClick={handleSync}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
立即同步
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={<BottomNav />}
|
||||
>
|
||||
<div className="bg-gray-50">
|
||||
<div className="p-4 space-y-6">
|
||||
@@ -137,7 +150,10 @@ export default function MomentsSyncDetail() {
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch checked={task.status === 1} onCheckedChange={handleToggleStatus} />
|
||||
<Switch
|
||||
checked={task.status === 1}
|
||||
onCheckedChange={handleToggleStatus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@@ -154,11 +170,17 @@ export default function MomentsSyncDetail() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">内容库</span>
|
||||
<span className="font-medium">{task.contentLib || '未设置'}</span>
|
||||
<span className="font-medium">
|
||||
{task.config?.contentLibraries
|
||||
.map((item: any) => item.name)
|
||||
.join(",")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">已同步</span>
|
||||
<span className="font-medium">{task.syncCount} 条</span>
|
||||
<span className="font-medium">
|
||||
{task.config?.syncCount} 条
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">创建人</span>
|
||||
@@ -180,11 +202,15 @@ export default function MomentsSyncDetail() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">上次同步</span>
|
||||
<span className="font-medium">{task.lastSyncTime || '暂无'}</span>
|
||||
<span className="font-medium">
|
||||
{task.lastSyncTime || "暂无"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">更新时间</span>
|
||||
<span className="font-medium">{task.updateTime || '暂无'}</span>
|
||||
<span className="font-medium">
|
||||
{task.updateTime || "暂无"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,35 +229,51 @@ export default function MomentsSyncDetail() {
|
||||
<span className="text-gray-600">同步间隔</span>
|
||||
<span className="font-medium">{task.syncInterval} 秒</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">每日最大</span>
|
||||
<span className="font-medium">{task.maxSyncPerDay || 100} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">每日最大</span>
|
||||
<span className="font-medium">
|
||||
{task.maxSyncPerDay || 100} 条
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">时间范围</span>
|
||||
<span className="font-medium">{task.timeRange.start} - {task.timeRange.end}</span>
|
||||
<span className="font-medium">
|
||||
{task.timeRange?.start && task.timeRange?.end
|
||||
? `${task.timeRange.start} - ${task.timeRange.end}`
|
||||
: "未设置"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">同步模式</span>
|
||||
<span className="font-medium">{task.syncMode === 'auto' ? '自动' : '手动'}</span>
|
||||
<span className="font-medium">
|
||||
{task.syncMode === "auto" ? "自动" : "手动"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">今日同步</span>
|
||||
<span className="font-medium">{task.todaySyncCount || 0} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">总同步数</span>
|
||||
<span className="font-medium">{task.totalSyncCount || task.syncCount || 0} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">今日同步</span>
|
||||
<span className="font-medium">
|
||||
{task.todaySyncCount || 0} 条
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">总同步数</span>
|
||||
<span className="font-medium">
|
||||
{task.totalSyncCount || task.syncCount || 0} 条
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">目标标签</span>
|
||||
<span className="font-medium">{task.targetTags.length} 个</span>
|
||||
<span className="font-medium">
|
||||
{task.targetTags?.length || 0} 个
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">内容类型</span>
|
||||
<span className="font-medium">{task.contentTypes.join(', ')}</span>
|
||||
<span className="font-medium">
|
||||
{task.contentTypes?.join(", ") || "未设置"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,11 +284,13 @@ export default function MomentsSyncDetail() {
|
||||
<h3 className="text-lg font-semibold mb-4">最近同步记录</h3>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">暂无同步记录</p>
|
||||
<p className="text-sm text-gray-400 mt-2">任务开始执行后将显示同步记录</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
任务开始执行后将显示同步记录
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
@@ -12,21 +12,21 @@ import {
|
||||
Copy,
|
||||
ChevronLeft,
|
||||
Share2,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import '@/components/Layout.css';
|
||||
import {
|
||||
fetchMomentsSyncTasks,
|
||||
deleteMomentsSyncTask,
|
||||
toggleMomentsSyncTask,
|
||||
} from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import "@/components/Layout.css";
|
||||
import {
|
||||
fetchMomentsSyncTasks,
|
||||
deleteMomentsSyncTask,
|
||||
toggleMomentsSyncTask,
|
||||
copyMomentsSyncTask,
|
||||
MomentsSyncTask
|
||||
} from '@/api/momentsSync';
|
||||
MomentsSyncTask,
|
||||
} from "@/api/momentsSync";
|
||||
|
||||
type CardMenuProps = {
|
||||
onView: () => void;
|
||||
@@ -51,7 +51,16 @@ function CardMenu({ onView, onEdit, onCopy, onDelete }: CardMenuProps) {
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setOpen((v) => !v)} style={{ background: "none", border: "none", padding: 0, margin: 0, cursor: "pointer" }}>
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</button>
|
||||
{open && (
|
||||
@@ -69,17 +78,106 @@ function CardMenu({ onView, onEdit, onCopy, onDelete }: CardMenuProps) {
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
<div onClick={() => { onView(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Eye className="h-4 w-4 mr-2" />查看
|
||||
<div
|
||||
onClick={() => {
|
||||
onView();
|
||||
setOpen(false);
|
||||
}}
|
||||
style={{
|
||||
padding: 8,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
gap: 6,
|
||||
transition: "background .2s",
|
||||
}}
|
||||
onMouseOver={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "#f5f5f5")
|
||||
}
|
||||
onMouseOut={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "")
|
||||
}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
查看
|
||||
</div>
|
||||
<div onClick={() => { onEdit(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Edit className="h-4 w-4 mr-2" />编辑
|
||||
<div
|
||||
onClick={() => {
|
||||
onEdit();
|
||||
setOpen(false);
|
||||
}}
|
||||
style={{
|
||||
padding: 8,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
gap: 6,
|
||||
transition: "background .2s",
|
||||
}}
|
||||
onMouseOver={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "#f5f5f5")
|
||||
}
|
||||
onMouseOut={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "")
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</div>
|
||||
<div onClick={() => { onCopy(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Copy className="h-4 w-4 mr-2" />复制
|
||||
<div
|
||||
onClick={() => {
|
||||
onCopy();
|
||||
setOpen(false);
|
||||
}}
|
||||
style={{
|
||||
padding: 8,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
gap: 6,
|
||||
transition: "background .2s",
|
||||
}}
|
||||
onMouseOver={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "#f5f5f5")
|
||||
}
|
||||
onMouseOut={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "")
|
||||
}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
复制
|
||||
</div>
|
||||
<div onClick={() => { onDelete(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, color: "#e53e3e", transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />删除
|
||||
<div
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
setOpen(false);
|
||||
}}
|
||||
style={{
|
||||
padding: 8,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
gap: 6,
|
||||
color: "#e53e3e",
|
||||
transition: "background .2s",
|
||||
}}
|
||||
onMouseOver={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "#f5f5f5")
|
||||
}
|
||||
onMouseOut={(e) =>
|
||||
((e.currentTarget as HTMLDivElement).style.background = "")
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
删除
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -90,7 +188,7 @@ function CardMenu({ onView, onEdit, onCopy, onDelete }: CardMenuProps) {
|
||||
export default function MomentsSync() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tasks, setTasks] = useState<MomentsSyncTask[]>([]);
|
||||
|
||||
@@ -100,24 +198,24 @@ export default function MomentsSync() {
|
||||
try {
|
||||
const list = await fetchMomentsSyncTasks();
|
||||
// 确保数据字段与界面一致
|
||||
const mappedTasks = list.map(task => ({
|
||||
const mappedTasks = list.map((task) => ({
|
||||
...task,
|
||||
// 确保字段名称和格式与界面一致
|
||||
status: task.status || 2, // 默认为关闭状态
|
||||
deviceCount: task.deviceCount || 0,
|
||||
targetGroup: task.targetGroup || '默认人群',
|
||||
targetGroup: task.targetGroup || "默认人群",
|
||||
syncCount: task.todaySyncCount || task.syncCount || 0,
|
||||
creatorName: task.creatorName || '未知',
|
||||
lastSyncTime: task.lastSyncTime || '暂无',
|
||||
createTime: task.createTime || '未知',
|
||||
creatorName: task.creatorName || "未知",
|
||||
lastSyncTime: task.lastSyncTime || "暂无",
|
||||
createTime: task.createTime || "未知",
|
||||
syncInterval: task.syncInterval || 30,
|
||||
maxSyncPerDay: task.maxSyncPerDay || 100,
|
||||
timeRange: task.timeRange || { start: '08:00', end: '22:00' },
|
||||
contentTypes: task.contentTypes || ['text', 'image', 'video'],
|
||||
timeRange: task.timeRange || { start: "08:00", end: "22:00" },
|
||||
contentTypes: task.contentTypes || ["text", "image", "video"],
|
||||
targetTags: task.targetTags || [],
|
||||
syncMode: task.syncMode || 'auto',
|
||||
syncMode: task.syncMode || "auto",
|
||||
filterKeywords: task.filterKeywords || [],
|
||||
contentLib: task.config?.contentLibraryNames?.join(',') || '默认内容库'
|
||||
contentLib: task.config?.contentLibraryNames?.join(",") || "默认内容库",
|
||||
}));
|
||||
setTasks(mappedTasks);
|
||||
} catch (error) {
|
||||
@@ -137,17 +235,25 @@ export default function MomentsSync() {
|
||||
if (!taskToDelete) return;
|
||||
|
||||
if (!window.confirm(`确定要删除"${taskToDelete.name}"吗?`)) return;
|
||||
|
||||
|
||||
try {
|
||||
const response = await deleteMomentsSyncTask(id);
|
||||
if (response.code === 200) {
|
||||
toast({ title: "删除成功" });
|
||||
fetchTasks();
|
||||
} else {
|
||||
toast({ title: "删除失败", description: response.msg || "请稍后重试", variant: "destructive" });
|
||||
toast({
|
||||
title: "删除失败",
|
||||
description: response.msg || "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: "删除失败", description: "请稍后重试", variant: "destructive" });
|
||||
toast({
|
||||
title: "删除失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,18 +272,26 @@ export default function MomentsSync() {
|
||||
toast({ title: "复制成功" });
|
||||
fetchTasks();
|
||||
} else {
|
||||
toast({ title: "复制失败", description: response.msg || "请稍后重试", variant: "destructive" });
|
||||
toast({
|
||||
title: "复制失败",
|
||||
description: response.msg || "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: "复制失败", description: "请稍后重试", variant: "destructive" });
|
||||
toast({
|
||||
title: "复制失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTaskStatus = async (id: string, status: number) => {
|
||||
// 先更新本地状态
|
||||
const newStatus = (status === 1 ? 2 : 1) as 1 | 2;
|
||||
setTasks(prevTasks =>
|
||||
prevTasks.map(task =>
|
||||
setTasks((prevTasks) =>
|
||||
prevTasks.map((task) =>
|
||||
task.id === id ? { ...task, status: newStatus } : task
|
||||
)
|
||||
);
|
||||
@@ -189,40 +303,48 @@ export default function MomentsSync() {
|
||||
// 成功时不刷新列表,保持本地状态
|
||||
} else {
|
||||
// 请求失败,回退本地状态
|
||||
setTasks(prevTasks =>
|
||||
prevTasks.map(task =>
|
||||
setTasks((prevTasks) =>
|
||||
prevTasks.map((task) =>
|
||||
task.id === id ? { ...task, status: status as 1 | 2 } : task
|
||||
)
|
||||
);
|
||||
toast({ title: "操作失败", description: response.msg || "请稍后重试", variant: "destructive" });
|
||||
toast({
|
||||
title: "操作失败",
|
||||
description: response.msg || "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 请求异常,回退本地状态
|
||||
setTasks(prevTasks =>
|
||||
prevTasks.map(task =>
|
||||
setTasks((prevTasks) =>
|
||||
prevTasks.map((task) =>
|
||||
task.id === id ? { ...task, status: status as 1 | 2 } : task
|
||||
)
|
||||
);
|
||||
toast({ title: "操作失败", description: "请稍后重试", variant: "destructive" });
|
||||
toast({
|
||||
title: "操作失败",
|
||||
description: "请稍后重试",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
navigate('/workspace/moments-sync/new');
|
||||
navigate("/workspace/moments-sync/new");
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter((task) =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '进行中';
|
||||
return "进行中";
|
||||
case 2:
|
||||
return '已暂停';
|
||||
return "已暂停";
|
||||
default:
|
||||
return '未知';
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,13 +353,18 @@ export default function MomentsSync() {
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate("/workspace")}
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">朋友圈同步</h1>
|
||||
</div>
|
||||
<Button onClick={handleCreateNew}>
|
||||
<Plus className="h-4 w-4 mr-2" />新建任务
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -246,14 +373,19 @@ export default function MomentsSync() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="搜索任务名称"
|
||||
className="pl-9"
|
||||
<Input
|
||||
placeholder="搜索任务名称"
|
||||
className="pl-9"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={fetchTasks} disabled={loading}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={fetchTasks}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
@@ -267,8 +399,12 @@ export default function MomentsSync() {
|
||||
{filteredTasks.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Share2 className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
||||
<p className="text-gray-500 text-lg font-medium mb-2">暂无同步任务</p>
|
||||
<p className="text-gray-400 text-sm mb-4">创建您的第一个朋友圈同步任务</p>
|
||||
<p className="text-gray-500 text-lg font-medium mb-2">
|
||||
暂无同步任务
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mb-4">
|
||||
创建您的第一个朋友圈同步任务
|
||||
</p>
|
||||
<Button onClick={handleCreateNew}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
创建第一个任务
|
||||
@@ -280,14 +416,20 @@ export default function MomentsSync() {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{task.name}</h3>
|
||||
<Badge variant={Number(task.status) === 1 ? "success" : "secondary"}>
|
||||
<Badge
|
||||
variant={
|
||||
Number(task.status) === 1 ? "success" : "secondary"
|
||||
}
|
||||
>
|
||||
{getStatusText(task.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={Number(task.status) === 1}
|
||||
onCheckedChange={() => toggleTaskStatus(task.id, Number(task.status))}
|
||||
onCheckedChange={() =>
|
||||
toggleTaskStatus(task.id, Number(task.status))
|
||||
}
|
||||
/>
|
||||
<CardMenu
|
||||
onView={() => handleView(task.id)}
|
||||
@@ -300,11 +442,14 @@ export default function MomentsSync() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>推送设备:{task?.config?.devices.length||0} 个</div>
|
||||
<div>推送设备:{task?.config?.devices.length || 0} 个</div>
|
||||
<div className="flex">
|
||||
<span className="flex-shrink-0">内容库:</span>
|
||||
<span className="truncate" title={task.contentLib || '默认内容库'}>
|
||||
{task.contentLib || '默认内容库'}
|
||||
<span
|
||||
className="truncate"
|
||||
title={task.contentLib || "默认内容库"}
|
||||
>
|
||||
{task.contentLib || "默认内容库"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,4 +473,4 @@ export default function MomentsSync() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Layout from '@/components/Layout';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RefreshCw, Search, RefreshCw as SyncIcon, Eye } from 'lucide-react';
|
||||
import { fetchMomentsSyncTasks, syncMoments, syncAllMoments, MomentsSyncTask } from '@/api/momentsSync';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Layout from "@/components/Layout";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { RefreshCw, Search, RefreshCw as SyncIcon, Eye } from "lucide-react";
|
||||
import {
|
||||
fetchMomentsSyncTasks,
|
||||
syncMoments,
|
||||
syncAllMoments,
|
||||
MomentsSyncTask,
|
||||
} from "@/api/momentsSync";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
export default function MomentsSyncPage() {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [tasks, setTasks] = useState<MomentsSyncTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const fetchTasks = async () => {
|
||||
setLoading(true);
|
||||
@@ -24,44 +28,46 @@ export default function MomentsSyncPage() {
|
||||
const list = await fetchMomentsSyncTasks();
|
||||
setTasks(list);
|
||||
} catch {
|
||||
toast({ title: '获取任务失败', variant: 'destructive' });
|
||||
toast({ title: "获取任务失败", variant: "destructive" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchTasks(); }, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchTasks();
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
setSearch('');
|
||||
setSearch("");
|
||||
fetchTasks();
|
||||
};
|
||||
|
||||
const handleSync = async (id: string) => {
|
||||
try {
|
||||
await syncMoments(id);
|
||||
toast({ title: '同步已发起' });
|
||||
toast({ title: "同步已发起" });
|
||||
fetchTasks();
|
||||
} catch {
|
||||
toast({ title: '同步失败', variant: 'destructive' });
|
||||
toast({ title: "同步失败", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncAll = async () => {
|
||||
try {
|
||||
await syncAllMoments();
|
||||
toast({ title: '全部同步已发起' });
|
||||
toast({ title: "全部同步已发起" });
|
||||
fetchTasks();
|
||||
} catch {
|
||||
toast({ title: '同步失败', variant: 'destructive' });
|
||||
toast({ title: "同步失败", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter(task =>
|
||||
const filteredTasks = tasks.filter((task) =>
|
||||
task.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
@@ -77,15 +83,18 @@ export default function MomentsSyncPage() {
|
||||
placeholder="搜索任务名称"
|
||||
className="pl-9"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSearch(); }}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSearch();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={handleRefresh}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={handleSyncAll} variant="default" size="sm">
|
||||
<SyncIcon className="h-4 w-4 mr-1" />全部同步
|
||||
<SyncIcon className="h-4 w-4 mr-1" />
|
||||
全部同步
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
@@ -94,26 +103,45 @@ export default function MomentsSyncPage() {
|
||||
) : filteredTasks.length === 0 ? (
|
||||
<Card className="p-8 text-center">暂无任务</Card>
|
||||
) : (
|
||||
filteredTasks.map(task => (
|
||||
<Card key={task.id} className="p-4 flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
filteredTasks.map((task) => (
|
||||
<Card
|
||||
key={task.id}
|
||||
className="p-4 flex flex-col md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<span className="font-medium text-base">{task.name}</span>
|
||||
<Badge variant={
|
||||
task.status === 1 ? 'success' : 'secondary'
|
||||
}>
|
||||
{task.status === 1 ? '进行中' : '已暂停'}
|
||||
<Badge
|
||||
variant={task.status === 1 ? "success" : "secondary"}
|
||||
>
|
||||
{task.status === 1 ? "进行中" : "已暂停"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mb-2">上次同步:{task.lastSyncTime || '无'}</div>
|
||||
<div className="text-xs text-gray-500 mb-2">已同步:{task.syncCount || 0} 条</div>
|
||||
<div className="text-xs text-gray-500 mb-2">
|
||||
上次同步:{task.lastSyncTime || "无"}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mb-2">
|
||||
已同步:{task.syncCount || 0} 条
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-2 md:mt-0">
|
||||
<Button size="sm" variant="outline" onClick={() => navigate(`/workspace/moments-sync/${task.id}`)}>
|
||||
<Eye className="h-4 w-4 mr-1" />查看
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
navigate(`/workspace/moments-sync/${task.id}`)
|
||||
}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSync(task.id)}>
|
||||
<SyncIcon className="h-4 w-4 mr-1" />同步
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleSync(task.id)}
|
||||
>
|
||||
<SyncIcon className="h-4 w-4 mr-1" />
|
||||
同步
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -123,4 +151,4 @@ export default function MomentsSyncPage() {
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { createMomentsSyncTask, updateMomentsSyncTask, fetchMomentsSyncTaskDetail } from '@/api/momentsSync';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { DeviceSelectionDialog } from '@/components/DeviceSelectionDialog';
|
||||
import { ContentLibrarySelectionDialog } from '@/components/ContentLibrarySelectionDialog';
|
||||
import { ContentType } from '@/types/moments-sync';
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
import {
|
||||
createMomentsSyncTask,
|
||||
updateMomentsSyncTask,
|
||||
fetchMomentsSyncTaskDetail,
|
||||
} from "@/api/momentsSync";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { DeviceSelectionDialog } from "@/components/DeviceSelectionDialog";
|
||||
import { ContentLibrarySelectionDialog } from "@/components/ContentLibrarySelectionDialog";
|
||||
import { ContentType } from "@/types/moments-sync";
|
||||
|
||||
// 步骤指示器组件
|
||||
interface StepIndicatorProps {
|
||||
@@ -25,36 +29,41 @@ function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="relative flex justify-between px-6 mb-8">
|
||||
{/* 连线 - 背景线 */}
|
||||
<div className="absolute top-5 left-0 right-0 h-[1px] bg-gray-200" style={{ width: '100%', zIndex: 0 }} />
|
||||
|
||||
{/* 连线 - 已完成线 */}
|
||||
<div
|
||||
className="absolute top-5 left-0 h-[1px] bg-blue-600 transition-all duration-300"
|
||||
style={{
|
||||
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
|
||||
zIndex: 1
|
||||
}}
|
||||
<div
|
||||
className="absolute top-5 left-0 right-0 h-[1px] bg-gray-200"
|
||||
style={{ width: "100%", zIndex: 0 }}
|
||||
/>
|
||||
|
||||
|
||||
{/* 连线 - 已完成线 */}
|
||||
<div
|
||||
className="absolute top-5 left-0 h-[1px] bg-blue-600 transition-all duration-300"
|
||||
style={{
|
||||
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 步骤圆圈 */}
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex flex-col items-center relative z-10">
|
||||
<div
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center mb-2 ${
|
||||
currentStep === index + 1
|
||||
? "bg-blue-600 text-white"
|
||||
currentStep === index + 1
|
||||
? "bg-blue-600 text-white"
|
||||
: index + 1 < currentStep
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-white text-gray-400 border border-gray-200"
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-white text-gray-400 border border-gray-200"
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className={`text-sm ${
|
||||
currentStep === index + 1 || index + 1 < currentStep
|
||||
? "text-blue-600"
|
||||
: "text-gray-400"
|
||||
}`}>
|
||||
<div
|
||||
className={`text-sm ${
|
||||
currentStep === index + 1 || index + 1 < currentStep
|
||||
? "text-blue-600"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,18 +79,19 @@ export default function NewMomentsSyncTask() {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false);
|
||||
const [contentLibraryDialogOpen, setContentLibraryDialogOpen] = useState(false);
|
||||
const [contentLibraryDialogOpen, setContentLibraryDialogOpen] =
|
||||
useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
taskName: '',
|
||||
startTime: '06:00',
|
||||
endTime: '23:59',
|
||||
taskName: "",
|
||||
startTime: "06:00",
|
||||
endTime: "23:59",
|
||||
syncCount: 5,
|
||||
interval: 30, // 同步间隔,单位:分钟
|
||||
accountType: 'business' as 'business' | 'personal',
|
||||
accountType: "business" as "business" | "personal",
|
||||
enabled: true,
|
||||
selectedDevices: [] as string[],
|
||||
selectedLibraries: [] as string[],
|
||||
contentTypes: ['text', 'image', 'video'] as ContentType[],
|
||||
contentTypes: ["text", "image", "video"] as ContentType[],
|
||||
targetTags: [] as string[],
|
||||
filterKeywords: [] as string[],
|
||||
});
|
||||
@@ -109,21 +119,21 @@ export default function NewMomentsSyncTask() {
|
||||
if (taskData) {
|
||||
setFormData({
|
||||
taskName: taskData.name,
|
||||
startTime: taskData.timeRange?.start || '06:00',
|
||||
endTime: taskData.timeRange?.end || '23:59',
|
||||
syncCount: taskData.maxSyncPerDay || 5,
|
||||
startTime: taskData.timeRange?.start || "06:00",
|
||||
endTime: taskData.timeRange?.end || "23:59",
|
||||
syncCount: taskData?.config?.syncCount || 0,
|
||||
interval: taskData.syncInterval || 30,
|
||||
accountType: taskData.syncMode === 'auto' ? 'business' : 'personal',
|
||||
accountType: taskData.syncMode === "auto" ? "business" : "personal",
|
||||
enabled: taskData.status === 1,
|
||||
selectedDevices: taskData.devices || [],
|
||||
selectedLibraries: taskData.contentLib ? [taskData.contentLib] : [],
|
||||
contentTypes: taskData.contentTypes || ['text', 'image', 'video'],
|
||||
contentTypes: taskData.contentTypes || ["text", "image", "video"],
|
||||
targetTags: taskData.targetTags || [],
|
||||
filterKeywords: taskData.filterKeywords || [],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: '获取任务详情失败', variant: 'destructive' });
|
||||
toast({ title: "获取任务详情失败", variant: "destructive" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -137,15 +147,15 @@ export default function NewMomentsSyncTask() {
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!formData.taskName.trim()) {
|
||||
toast({ title: '请输入任务名称', variant: 'destructive' });
|
||||
toast({ title: "请输入任务名称", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
if (formData.selectedDevices.length === 0) {
|
||||
toast({ title: '请选择设备', variant: 'destructive' });
|
||||
toast({ title: "请选择设备", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
if (formData.selectedLibraries.length === 0) {
|
||||
toast({ title: '请选择内容库', variant: 'destructive' });
|
||||
toast({ title: "请选择内容库", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,10 +167,10 @@ export default function NewMomentsSyncTask() {
|
||||
contentLibraries: formData.selectedLibraries,
|
||||
syncInterval: formData.interval,
|
||||
syncCount: formData.syncCount,
|
||||
syncType: formData.accountType === 'business' ? 1 : 2,
|
||||
syncType: formData.accountType === "business" ? 1 : 2,
|
||||
startTime: formData.startTime,
|
||||
endTime: formData.endTime,
|
||||
accountType: formData.accountType === 'business' ? 1 : 2,
|
||||
accountType: formData.accountType === "business" ? 1 : 2,
|
||||
contentTypes: formData.contentTypes,
|
||||
targetTags: formData.targetTags,
|
||||
filterKeywords: formData.filterKeywords,
|
||||
@@ -171,15 +181,18 @@ export default function NewMomentsSyncTask() {
|
||||
id,
|
||||
...taskData,
|
||||
});
|
||||
toast({ title: '更新成功' });
|
||||
toast({ title: "更新成功" });
|
||||
navigate(`/workspace/moments-sync/${id}`);
|
||||
} else {
|
||||
await createMomentsSyncTask(taskData);
|
||||
toast({ title: '创建成功' });
|
||||
navigate('/workspace/moments-sync');
|
||||
toast({ title: "创建成功" });
|
||||
navigate("/workspace/moments-sync");
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: isEditMode ? '更新失败' : '创建失败', variant: 'destructive' });
|
||||
toast({
|
||||
title: isEditMode ? "更新失败" : "创建失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -202,8 +215,12 @@ export default function NewMomentsSyncTask() {
|
||||
<div className="mb-6">
|
||||
<div className="text-base font-medium mb-2">每日同步数量</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => handleUpdateFormData({ syncCount: Math.max(1, formData.syncCount - 1) })}
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUpdateFormData({
|
||||
syncCount: Math.max(1, formData.syncCount - 1),
|
||||
})
|
||||
}
|
||||
className="w-12 h-12 rounded-lg bg-white border border-gray-200 flex items-center justify-center text-xl"
|
||||
>
|
||||
-
|
||||
@@ -211,8 +228,10 @@ export default function NewMomentsSyncTask() {
|
||||
<div className="flex-1 text-center text-lg font-medium">
|
||||
{formData.syncCount}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUpdateFormData({ syncCount: formData.syncCount + 1 })}
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUpdateFormData({ syncCount: formData.syncCount + 1 })
|
||||
}
|
||||
className="w-12 h-12 rounded-lg bg-white border border-gray-200 flex items-center justify-center text-xl"
|
||||
>
|
||||
+
|
||||
@@ -224,8 +243,12 @@ export default function NewMomentsSyncTask() {
|
||||
<div className="mb-6">
|
||||
<div className="text-base font-medium mb-2">同步间隔</div>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => handleUpdateFormData({ interval: Math.max(1, formData.interval - 1) })}
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUpdateFormData({
|
||||
interval: Math.max(1, formData.interval - 1),
|
||||
})
|
||||
}
|
||||
className="w-12 h-12 rounded-lg bg-white border border-gray-200 flex items-center justify-center text-xl"
|
||||
>
|
||||
-
|
||||
@@ -233,15 +256,19 @@ export default function NewMomentsSyncTask() {
|
||||
<div className="flex-1 text-center text-lg font-medium">
|
||||
{formData.interval}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleUpdateFormData({ interval: formData.interval + 1 })}
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUpdateFormData({ interval: formData.interval + 1 })
|
||||
}
|
||||
className="w-12 h-12 rounded-lg bg-white border border-gray-200 flex items-center justify-center text-xl"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<span className="ml-2 text-gray-500">分钟</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">设置每次发朋友圈的时间间隔</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
设置每次发朋友圈的时间间隔
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
@@ -251,7 +278,9 @@ export default function NewMomentsSyncTask() {
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => handleUpdateFormData({ startTime: e.target.value })}
|
||||
onChange={(e) =>
|
||||
handleUpdateFormData({ startTime: e.target.value })
|
||||
}
|
||||
className="h-12 rounded-lg border-gray-200 text-base"
|
||||
/>
|
||||
</div>
|
||||
@@ -260,7 +289,9 @@ export default function NewMomentsSyncTask() {
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => handleUpdateFormData({ endTime: e.target.value })}
|
||||
onChange={(e) =>
|
||||
handleUpdateFormData({ endTime: e.target.value })
|
||||
}
|
||||
className="h-12 rounded-lg border-gray-200 text-base"
|
||||
/>
|
||||
</div>
|
||||
@@ -297,7 +328,9 @@ export default function NewMomentsSyncTask() {
|
||||
<span className="text-base font-medium">是否启用</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) => handleUpdateFormData({ enabled: checked })}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateFormData({ enabled: checked })
|
||||
}
|
||||
className="data-[state=checked]:bg-blue-600 h-7 w-12"
|
||||
/>
|
||||
</div>
|
||||
@@ -316,7 +349,12 @@ export default function NewMomentsSyncTask() {
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)} className="hover:bg-gray-50">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => navigate(-1)}
|
||||
className="hover:bg-gray-50"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h1 className="ml-2 text-lg font-medium">新建朋友圈同步</h1>
|
||||
@@ -327,7 +365,7 @@ export default function NewMomentsSyncTask() {
|
||||
<StepIndicator currentStep={currentStep} />
|
||||
|
||||
{currentStep === 1 && renderBasicSettings()}
|
||||
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div className="px-4 space-y-6">
|
||||
<Input
|
||||
@@ -336,15 +374,19 @@ export default function NewMomentsSyncTask() {
|
||||
onClick={() => setDeviceDialogOpen(true)}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
|
||||
{formData.selectedDevices.length > 0 && (
|
||||
<div className="text-base text-gray-500">
|
||||
已选设备:{formData.selectedDevices.length} 个
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePrev}
|
||||
className="flex-1 h-12 rounded-lg"
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
@@ -354,7 +396,7 @@ export default function NewMomentsSyncTask() {
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<DeviceSelectionDialog
|
||||
open={deviceDialogOpen}
|
||||
onOpenChange={setDeviceDialogOpen}
|
||||
@@ -365,7 +407,7 @@ export default function NewMomentsSyncTask() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{currentStep === 3 && (
|
||||
<div className="px-4 space-y-6">
|
||||
<Input
|
||||
@@ -374,15 +416,19 @@ export default function NewMomentsSyncTask() {
|
||||
onClick={() => setContentLibraryDialogOpen(true)}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
|
||||
{formData.selectedLibraries.length > 0 && (
|
||||
<div className="text-base text-gray-500">
|
||||
已选内容库:{formData.selectedLibraries.length} 个
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex space-x-4 pt-4">
|
||||
<Button variant="outline" onClick={handlePrev} className="flex-1 h-12 rounded-lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePrev}
|
||||
className="flex-1 h-12 rounded-lg"
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
@@ -390,10 +436,10 @@ export default function NewMomentsSyncTask() {
|
||||
loading={loading}
|
||||
className="flex-1 h-12 bg-blue-500 hover:bg-blue-600 rounded-lg text-white"
|
||||
>
|
||||
{loading ? '创建中...' : '完成'}
|
||||
{loading ? "创建中..." : "完成"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<ContentLibrarySelectionDialog
|
||||
open={contentLibraryDialogOpen}
|
||||
onOpenChange={setContentLibraryDialogOpen}
|
||||
@@ -407,4 +453,4 @@ export default function NewMomentsSyncTask() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,17 +26,17 @@ export interface MomentsSyncTask {
|
||||
creatorName: string;
|
||||
syncInterval: number;
|
||||
maxSyncPerDay: number;
|
||||
timeRange: { start: string; end: string };
|
||||
contentTypes: ContentType[];
|
||||
targetTags: string[];
|
||||
timeRange?: { start: string; end: string };
|
||||
contentTypes?: ContentType[];
|
||||
targetTags?: string[];
|
||||
syncMode: SyncMode;
|
||||
filterKeywords: string[];
|
||||
filterKeywords?: string[];
|
||||
contentLib?: string;
|
||||
devices: string[];
|
||||
friends: string[];
|
||||
todaySyncCount: number;
|
||||
totalSyncCount: number;
|
||||
updateTime: string;
|
||||
devices?: string[];
|
||||
friends?: string[];
|
||||
todaySyncCount?: number;
|
||||
totalSyncCount?: number;
|
||||
updateTime?: string;
|
||||
config?: MomentsSyncConfig;
|
||||
}
|
||||
|
||||
|
||||
14
nkebao.code-workspace
Normal file
14
nkebao.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "nkebao"
|
||||
},
|
||||
{
|
||||
"path": "Cunkebao"
|
||||
},
|
||||
{
|
||||
"path": "../../MySelf/好版登项目/好版登小程序"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
4
nkebao/.env.development
Normal file
4
nkebao/.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
4
nkebao/.env.production
Normal file
4
nkebao/.env.production
Normal file
@@ -0,0 +1,4 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
21
nkebao/.eslintrc.js
Normal file
21
nkebao/.eslintrc.js
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'react-app',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react', '@typescript-eslint', 'prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
};
|
||||
6
nkebao/.gitignore
vendored
Normal file
6
nkebao/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
yarn.lock
|
||||
.env
|
||||
.DS_Store
|
||||
274
nkebao/AI_ICON_GUIDE.md
Normal file
274
nkebao/AI_ICON_GUIDE.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# AI图标使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档为AI助手提供标准化的图标选择和使用流程,确保在代码生成时使用正确的图标包和图标名称。
|
||||
|
||||
## AI使用流程
|
||||
|
||||
### 1. 项目类型判断
|
||||
首先判断项目类型:
|
||||
- **PC端项目**: 使用 `@ant-design/icons`
|
||||
- **移动端项目**: 使用 `antd-mobile-icons`
|
||||
|
||||
### 2. 图标查找流程
|
||||
1. 根据功能需求确定图标类型
|
||||
2. 在对应图标包中查找合适的图标
|
||||
3. 如果图标不存在,查找替代方案
|
||||
4. 使用正确的导入语法
|
||||
|
||||
### 3. 代码生成模板
|
||||
|
||||
#### PC端项目模板
|
||||
```typescript
|
||||
import {
|
||||
// 导航类
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
|
||||
// 操作类
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
|
||||
// 状态类
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
LoadingOutlined,
|
||||
|
||||
// 方向类
|
||||
UpOutlined,
|
||||
DownOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
ArrowLeftOutlined,
|
||||
|
||||
// 其他
|
||||
MessageOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
PictureOutlined,
|
||||
FileOutlined,
|
||||
CameraOutlined,
|
||||
QrcodeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
```
|
||||
|
||||
#### 移动端项目模板
|
||||
```typescript
|
||||
import {
|
||||
// 导航类
|
||||
HomeOutline,
|
||||
UserOutline,
|
||||
SettingOutline,
|
||||
|
||||
// 操作类
|
||||
AddOutline,
|
||||
EditSOutline,
|
||||
DeleteOutline,
|
||||
CopyOutline,
|
||||
SearchOutline,
|
||||
RefreshOutline,
|
||||
|
||||
// 状态类
|
||||
CheckOutline,
|
||||
CloseOutline,
|
||||
CheckCircleOutline,
|
||||
CloseCircleOutline,
|
||||
ExclamationCircleOutline,
|
||||
InfoCircleOutline,
|
||||
LoadingOutline,
|
||||
|
||||
// 方向类
|
||||
UpOutline,
|
||||
DownOutline,
|
||||
LeftOutline,
|
||||
RightOutline,
|
||||
|
||||
// 其他
|
||||
MessageOutline,
|
||||
CalendarOutline,
|
||||
ClockCircleOutline,
|
||||
PictureOutline,
|
||||
FileOutline,
|
||||
CameraOutline,
|
||||
QrCodeOutline,
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
## 功能到图标映射
|
||||
|
||||
### 基础功能映射
|
||||
| 功能需求 | PC端图标 | 移动端图标 | 说明 |
|
||||
|---------|---------|-----------|------|
|
||||
| 添加/新建 | PlusOutlined | AddOutline | 通用添加功能 |
|
||||
| 编辑/修改 | EditOutlined | EditSOutline | 编辑功能 |
|
||||
| 删除/移除 | DeleteOutlined | DeleteOutline | 删除功能 |
|
||||
| 复制/克隆 | CopyOutlined | CopyOutline | 复制功能 |
|
||||
| 搜索/查找 | SearchOutlined | SearchOutline | 搜索功能 |
|
||||
| 刷新/重新加载 | ReloadOutlined | RefreshOutline | 刷新功能 |
|
||||
| 设置/配置 | SettingOutlined | SettingOutline | 设置功能 |
|
||||
| 用户/个人 | UserOutlined | UserOutline | 用户相关 |
|
||||
| 首页/主页 | HomeOutlined | HomeOutline | 首页导航 |
|
||||
| 返回/后退 | ArrowLeftOutlined | LeftOutline | 返回功能 |
|
||||
| 关闭/取消 | CloseOutlined | CloseOutline | 关闭功能 |
|
||||
| 确认/确定 | CheckOutlined | CheckOutline | 确认功能 |
|
||||
|
||||
### 状态指示映射
|
||||
| 状态需求 | PC端图标 | 移动端图标 | 说明 |
|
||||
|---------|---------|-----------|------|
|
||||
| 成功/完成 | CheckCircleOutlined | CheckCircleOutline | 成功状态 |
|
||||
| 错误/失败 | CloseCircleOutlined | CloseCircleOutline | 错误状态 |
|
||||
| 警告/注意 | ExclamationCircleOutlined | ExclamationCircleOutline | 警告状态 |
|
||||
| 信息/提示 | InfoCircleOutlined | InfoCircleOutline | 信息提示 |
|
||||
| 加载/等待 | LoadingOutlined | LoadingOutline | 加载状态 |
|
||||
| 时间/等待 | ClockCircleOutlined | ClockCircleOutline | 时间相关 |
|
||||
|
||||
### 方向导航映射
|
||||
| 方向需求 | PC端图标 | 移动端图标 | 说明 |
|
||||
|---------|---------|-----------|------|
|
||||
| 向上/上升 | UpOutlined | UpOutline | 向上方向 |
|
||||
| 向下/下降 | DownOutlined | DownOutline | 向下方向 |
|
||||
| 向左/后退 | LeftOutlined | LeftOutline | 向左方向 |
|
||||
| 向右/前进 | RightOutlined | RightOutline | 向右方向 |
|
||||
|
||||
### 业务功能映射
|
||||
| 业务需求 | PC端图标 | 移动端图标 | 说明 |
|
||||
|---------|---------|-----------|------|
|
||||
| 消息/通知 | MessageOutlined | MessageOutline | 消息功能 |
|
||||
| 日历/日期 | CalendarOutlined | CalendarOutline | 日历功能 |
|
||||
| 图片/照片 | PictureOutlined | PictureOutline | 图片功能 |
|
||||
| 文件/文档 | FileOutlined | FileOutline | 文件功能 |
|
||||
| 相机/拍照 | CameraOutlined | CameraOutline | 相机功能 |
|
||||
| 二维码 | QrcodeOutlined | QrCodeOutline | 二维码功能 |
|
||||
| 微信/社交 | WechatOutlined | WechatOutline | 微信功能 |
|
||||
| 设备/手机 | MobileOutlined | MobileOutline | 设备功能 |
|
||||
| 团队/群组 | TeamOutlined | TeamOutline | 团队功能 |
|
||||
| 订单/购物 | ShoppingOutlined | ShoppingOutline | 订单功能 |
|
||||
| 支付/钱包 | PayCircleOutlined | PayCircleOutline | 支付功能 |
|
||||
|
||||
## 特殊替换规则
|
||||
|
||||
### 移动端不存在的图标替换
|
||||
| 原需求 | 替换方案 | 说明 |
|
||||
|--------|----------|------|
|
||||
| RiseOutlined | UpOutline | 上升趋势 |
|
||||
| ThumbsUpOutlined | LikeOutline | 点赞功能 |
|
||||
| ShareAltOutlined | LinkOutline | 分享功能 |
|
||||
| BarChartOutlined | PieOutline | 图表功能 |
|
||||
| LineChartOutlined | PieOutline | 图表功能 |
|
||||
| UserAddOutlined | UserOutline | 用户添加 |
|
||||
| Progress | 自定义div | 进度条组件 |
|
||||
|
||||
### PC端不存在的图标替换
|
||||
| 原需求 | 替换方案 | 说明 |
|
||||
|--------|----------|------|
|
||||
| AntOutline | HomeOutlined | 蚂蚁图标 |
|
||||
| AppOutline | AppstoreOutlined | 应用图标 |
|
||||
|
||||
## AI代码生成示例
|
||||
|
||||
### 场景1: 移动端列表页面
|
||||
```typescript
|
||||
// AI应该生成的代码
|
||||
import {
|
||||
AddOutline, // 添加按钮
|
||||
EditSOutline, // 编辑按钮
|
||||
DeleteOutline, // 删除按钮
|
||||
CopyOutline, // 复制按钮
|
||||
SearchOutline, // 搜索框
|
||||
RefreshOutline, // 刷新按钮
|
||||
UserOutline, // 用户信息
|
||||
CalendarOutline, // 时间信息
|
||||
UpOutline, // 上升趋势(替换RiseOutlined)
|
||||
} from 'antd-mobile-icons';
|
||||
|
||||
// 使用示例
|
||||
<Button>
|
||||
<AddOutline /> 新建
|
||||
</Button>
|
||||
<Button>
|
||||
<EditSOutline /> 编辑
|
||||
</Button>
|
||||
<Button color="danger">
|
||||
<DeleteOutline /> 删除
|
||||
</Button>
|
||||
```
|
||||
|
||||
### 场景2: PC端管理页面
|
||||
```typescript
|
||||
// AI应该生成的代码
|
||||
import {
|
||||
PlusOutlined, // 添加按钮
|
||||
EditOutlined, // 编辑按钮
|
||||
DeleteOutlined, // 删除按钮
|
||||
CopyOutlined, // 复制按钮
|
||||
SearchOutlined, // 搜索框
|
||||
ReloadOutlined, // 刷新按钮
|
||||
UserOutlined, // 用户信息
|
||||
CalendarOutlined, // 时间信息
|
||||
RiseOutlined, // 上升趋势(PC端存在)
|
||||
} from '@ant-design/icons';
|
||||
|
||||
// 使用示例
|
||||
<Button icon={<PlusOutlined />}>新建</Button>
|
||||
<Button icon={<EditOutlined />}>编辑</Button>
|
||||
<Button icon={<DeleteOutlined />} danger>删除</Button>
|
||||
```
|
||||
|
||||
## 错误检测和修正
|
||||
|
||||
### 常见错误模式
|
||||
1. **混用图标包**: 同时导入PC端和移动端图标
|
||||
2. **使用不存在的图标**: 在移动端使用PC端特有的图标
|
||||
3. **命名错误**: 图标名称大小写错误
|
||||
4. **导入路径错误**: 使用错误的包名
|
||||
|
||||
### 修正策略
|
||||
1. **统一图标包**: 根据项目类型选择单一图标包
|
||||
2. **查找替代**: 使用对照表查找替代图标
|
||||
3. **验证存在**: 确保图标在目标包中存在
|
||||
4. **测试验证**: 在代码中测试图标是否正常显示
|
||||
|
||||
## AI使用检查清单
|
||||
|
||||
### 代码生成前
|
||||
- [ ] 确认项目类型(PC端/移动端)
|
||||
- [ ] 选择对应的图标包
|
||||
- [ ] 根据功能需求选择合适图标
|
||||
- [ ] 检查图标是否存在
|
||||
|
||||
### 代码生成中
|
||||
- [ ] 使用正确的导入语法
|
||||
- [ ] 图标名称大小写正确
|
||||
- [ ] 避免混用不同包的图标
|
||||
- [ ] 为不存在的图标提供替代方案
|
||||
|
||||
### 代码生成后
|
||||
- [ ] 验证图标导入正确
|
||||
- [ ] 检查图标使用语法
|
||||
- [ ] 确保样式设置合理
|
||||
- [ ] 提供使用示例
|
||||
|
||||
## 更新和维护
|
||||
|
||||
- 定期更新图标对照表
|
||||
- 记录新发现的图标差异
|
||||
- 更新替换规则
|
||||
- 优化AI使用流程
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **优先使用语义化图标**: 选择最能表达功能的图标
|
||||
2. **保持一致性**: 在同一项目中保持图标风格一致
|
||||
3. **考虑可访问性**: 为图标添加适当的aria-label
|
||||
4. **性能优化**: 按需导入图标,避免全量导入
|
||||
5. **版本兼容**: 注意图标包版本与UI框架版本的兼容性
|
||||
205
nkebao/ICON_DETAILED_MAPPING.md
Normal file
205
nkebao/ICON_DETAILED_MAPPING.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 详细图标对照表
|
||||
|
||||
## 基础图标对照
|
||||
|
||||
### 导航类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 首页 | HomeOutlined | HomeOutline | ✅ |
|
||||
| 返回 | ArrowLeftOutlined | LeftOutline | ✅ |
|
||||
| 菜单 | MenuOutlined | MenuOutline | ✅ |
|
||||
| 设置 | SettingOutlined | SettingOutline | ✅ |
|
||||
| 用户 | UserOutlined | UserOutline | ✅ |
|
||||
| 个人中心 | UserOutlined | UserOutline | ✅ |
|
||||
|
||||
### 操作类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 添加 | PlusOutlined | AddOutline | ✅ |
|
||||
| 编辑 | EditOutlined | EditSOutline | ✅ |
|
||||
| 删除 | DeleteOutlined | DeleteOutline | ✅ |
|
||||
| 复制 | CopyOutlined | CopyOutline | ✅ |
|
||||
| 保存 | SaveOutlined | SaveOutline | ✅ |
|
||||
| 刷新 | ReloadOutlined | RefreshOutline | ✅ |
|
||||
| 搜索 | SearchOutlined | SearchOutline | ✅ |
|
||||
| 关闭 | CloseOutlined | CloseOutline | ✅ |
|
||||
| 确认 | CheckOutlined | CheckOutline | ✅ |
|
||||
| 取消 | CloseOutlined | CloseOutline | ✅ |
|
||||
|
||||
### 状态类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 成功 | CheckCircleOutlined | CheckCircleOutline | ✅ |
|
||||
| 错误 | CloseCircleOutlined | CloseCircleOutline | ✅ |
|
||||
| 警告 | ExclamationCircleOutlined | ExclamationCircleOutline | ✅ |
|
||||
| 信息 | InfoCircleOutlined | InfoCircleOutline | ✅ |
|
||||
| 加载 | LoadingOutlined | LoadingOutline | ✅ |
|
||||
| 等待 | ClockCircleOutlined | ClockCircleOutline | ✅ |
|
||||
|
||||
### 方向类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 向上 | UpOutlined | UpOutline | ✅ |
|
||||
| 向下 | DownOutlined | DownOutline | ✅ |
|
||||
| 向左 | LeftOutlined | LeftOutline | ✅ |
|
||||
| 向右 | RightOutlined | RightOutline | ✅ |
|
||||
| 向上圆形 | UpCircleOutlined | UpCircleOutline | ✅ |
|
||||
| 向下圆形 | DownCircleOutlined | DownCircleOutline | ✅ |
|
||||
|
||||
### 通信类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 消息 | MessageOutlined | MessageOutline | ✅ |
|
||||
| 邮件 | MailOutlined | MailOutline | ✅ |
|
||||
| 电话 | PhoneOutlined | PhoneOutline | ✅ |
|
||||
| 视频 | VideoCameraOutlined | VideoCameraOutline | ✅ |
|
||||
| 语音 | AudioOutlined | AudioOutline | ✅ |
|
||||
|
||||
### 媒体类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 图片 | PictureOutlined | PictureOutline | ✅ |
|
||||
| 文件 | FileOutlined | FileOutline | ✅ |
|
||||
| 文件夹 | FolderOutlined | FolderOutline | ✅ |
|
||||
| 相机 | CameraOutlined | CameraOutline | ✅ |
|
||||
| 二维码 | QrcodeOutlined | QrCodeOutline | ✅ |
|
||||
|
||||
### 时间类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 日历 | CalendarOutlined | CalendarOutline | ✅ |
|
||||
| 时钟 | ClockCircleOutlined | ClockCircleOutline | ✅ |
|
||||
| 历史 | HistoryOutlined | HistoryOutline | ✅ |
|
||||
|
||||
### 数据类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 统计 | BarChartOutlined | BarChartOutline | ✅ |
|
||||
| 饼图 | PieChartOutlined | PieChartOutline | ✅ |
|
||||
| 折线图 | LineChartOutlined | LineChartOutline | ✅ |
|
||||
| 表格 | TableOutlined | TableOutline | ✅ |
|
||||
| 列表 | UnorderedListOutlined | UnorderedListOutline | ✅ |
|
||||
|
||||
## 特殊图标对照
|
||||
|
||||
### 业务相关
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 设备 | MobileOutlined | MobileOutline | ✅ |
|
||||
| 微信 | WechatOutlined | WechatOutline | ✅ |
|
||||
| 群组 | TeamOutlined | TeamOutline | ✅ |
|
||||
| 客户 | UserAddOutlined | UserAddOutline | ❌ |
|
||||
| 订单 | ShoppingOutlined | ShoppingOutline | ✅ |
|
||||
| 支付 | PayCircleOutlined | PayCircleOutline | ✅ |
|
||||
|
||||
### 工具类
|
||||
| 功能 | PC端 | 移动端 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 工具 | ToolOutlined | ToolOutline | ✅ |
|
||||
| 配置 | SettingOutlined | SettingOutline | ✅ |
|
||||
| 帮助 | QuestionCircleOutlined | QuestionCircleOutline | ✅ |
|
||||
| 反馈 | MessageOutlined | MessageOutline | ✅ |
|
||||
| 分享 | ShareAltOutlined | ShareOutline | ❌ |
|
||||
|
||||
## 不存在的图标替换方案
|
||||
|
||||
### PC端存在但移动端不存在的图标
|
||||
| PC端图标 | 推荐替换 | 说明 |
|
||||
|----------|----------|------|
|
||||
| UserAddOutlined | UserOutline | 用户添加功能 |
|
||||
| ShareAltOutlined | LinkOutline | 分享功能 |
|
||||
| RiseOutlined | UpOutline | 上升趋势 |
|
||||
| ThumbsUpOutlined | LikeOutline | 点赞功能 |
|
||||
| BarChartOutlined | PieOutline | 图表功能 |
|
||||
| LineChartOutlined | PieOutline | 图表功能 |
|
||||
|
||||
### 移动端存在但PC端不存在的图标
|
||||
| 移动端图标 | 推荐替换 | 说明 |
|
||||
|------------|----------|------|
|
||||
| AntOutline | HomeOutlined | 蚂蚁图标 |
|
||||
| AppOutline | AppstoreOutlined | 应用图标 |
|
||||
|
||||
## 使用规范
|
||||
|
||||
### 1. 导入规范
|
||||
```typescript
|
||||
// PC端项目
|
||||
import {
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
// 移动端项目
|
||||
import {
|
||||
HomeOutline,
|
||||
UserOutline,
|
||||
SettingOutline,
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
### 2. 命名规范
|
||||
- PC端:使用 `Outlined` 后缀
|
||||
- 移动端:使用 `Outline` 后缀
|
||||
- 保持语义化命名
|
||||
|
||||
### 3. 使用建议
|
||||
- 优先使用语义明确的图标
|
||||
- 保持图标风格一致性
|
||||
- 考虑图标在不同尺寸下的清晰度
|
||||
- 为图标添加适当的aria-label
|
||||
|
||||
### 4. 错误处理
|
||||
当图标不存在时:
|
||||
1. 查找语义相近的图标
|
||||
2. 使用通用图标(如QuestionOutlined)
|
||||
3. 考虑使用文字替代
|
||||
4. 创建自定义图标组件
|
||||
|
||||
## 项目中的实际应用
|
||||
|
||||
### 场景获客模块使用的图标
|
||||
```typescript
|
||||
// 移动端项目中的图标使用
|
||||
import {
|
||||
AddOutline, // 添加
|
||||
UpOutline, // 上升趋势(替换RiseOutline)
|
||||
UserOutline, // 用户
|
||||
CalendarOutline, // 日历
|
||||
CopyOutline, // 复制
|
||||
DeleteOutline, // 删除
|
||||
EditSOutline, // 编辑
|
||||
SettingOutline, // 设置
|
||||
SearchOutline, // 搜索
|
||||
RefreshOutline, // 刷新
|
||||
QrCodeOutline, // 二维码
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
### 工作台模块使用的图标
|
||||
```typescript
|
||||
// 移动端项目中的图标使用
|
||||
import {
|
||||
LikeOutline, // 点赞(替换ThumbsUpOutline)
|
||||
LinkOutline, // 链接(替换ShareOutline)
|
||||
PieOutline, // 饼图(替换BarChartOutline/LineChartOutline)
|
||||
UserOutline, // 用户
|
||||
TeamOutline, // 团队
|
||||
MessageOutline, // 消息
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
## 更新和维护
|
||||
|
||||
1. **定期检查**: 定期检查新版本中新增的图标
|
||||
2. **文档更新**: 及时更新图标对照表
|
||||
3. **团队协作**: 团队成员共享图标使用规范
|
||||
4. **代码审查**: 在代码审查中检查图标使用是否正确
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **包版本**: 确保图标包版本与UI框架版本兼容
|
||||
2. **按需导入**: 避免全量导入图标,影响打包体积
|
||||
3. **样式覆盖**: 可以通过CSS自定义图标样式
|
||||
4. **无障碍**: 为图标添加适当的无障碍属性
|
||||
5. **性能**: 大量使用图标时注意性能优化
|
||||
230
nkebao/ICON_MAPPING_GUIDE.md
Normal file
230
nkebao/ICON_MAPPING_GUIDE.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# PC端与移动端图标对照文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了PC端(@ant-design/icons)和移动端(antd-mobile-icons)的图标名称对照,以及正确的导入方式。
|
||||
|
||||
## 导入方式
|
||||
|
||||
### PC端图标 (@ant-design/icons)
|
||||
```typescript
|
||||
import {
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
// ... 其他图标
|
||||
} from '@ant-design/icons';
|
||||
```
|
||||
|
||||
### 移动端图标 (antd-mobile-icons)
|
||||
```typescript
|
||||
import {
|
||||
AntOutline,
|
||||
ArrowDownCircleOutline,
|
||||
UserOutline,
|
||||
// ... 其他图标
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
## 图标对照表
|
||||
|
||||
### 常用图标对照
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 | 备注 |
|
||||
|---------|---------|-----------|------|
|
||||
| 首页 | HomeOutlined | HomeOutline | 完全对应 |
|
||||
| 用户 | UserOutlined | UserOutline | 完全对应 |
|
||||
| 设置 | SettingOutlined | SettingOutline | 完全对应 |
|
||||
| 搜索 | SearchOutlined | SearchOutline | 完全对应 |
|
||||
| 添加 | PlusOutlined | AddOutline | 完全对应 |
|
||||
| 编辑 | EditOutlined | EditSOutline | 移动端略有不同 |
|
||||
| 删除 | DeleteOutlined | DeleteOutline | 完全对应 |
|
||||
| 复制 | CopyOutlined | CopyOutline | 完全对应 |
|
||||
| 刷新 | ReloadOutlined | RefreshOutline | 完全对应 |
|
||||
| 二维码 | QrcodeOutlined | QrCodeOutline | 完全对应 |
|
||||
| 日历 | CalendarOutlined | CalendarOutline | 完全对应 |
|
||||
| 时钟 | ClockCircleOutlined | ClockCircleOutline | 完全对应 |
|
||||
| 箭头向上 | UpOutlined | UpOutline | 完全对应 |
|
||||
| 箭头向下 | DownOutlined | DownOutline | 完全对应 |
|
||||
| 箭头向左 | LeftOutlined | LeftOutline | 完全对应 |
|
||||
| 箭头向右 | RightOutlined | RightOutline | 完全对应 |
|
||||
| 返回 | ArrowLeftOutlined | LeftOutline | 移动端使用LeftOutline |
|
||||
| 关闭 | CloseOutlined | CloseOutline | 完全对应 |
|
||||
| 检查 | CheckOutlined | CheckOutline | 完全对应 |
|
||||
| 警告 | ExclamationCircleOutlined | ExclamationCircleOutline | 完全对应 |
|
||||
| 信息 | InfoCircleOutlined | InfoCircleOutline | 完全对应 |
|
||||
| 成功 | CheckCircleOutlined | CheckCircleOutline | 完全对应 |
|
||||
| 错误 | CloseCircleOutlined | CloseCircleOutline | 完全对应 |
|
||||
|
||||
### 方向性图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 向上 | UpOutlined | UpOutline |
|
||||
| 向下 | DownOutlined | DownOutline |
|
||||
| 向左 | LeftOutlined | LeftOutline |
|
||||
| 向右 | RightOutlined | RightOutline |
|
||||
| 向上圆形 | UpCircleOutlined | UpCircleOutline |
|
||||
| 向下圆形 | DownCircleOutlined | DownCircleOutline |
|
||||
| 向左圆形 | LeftCircleOutlined | LeftCircleOutline |
|
||||
| 向右圆形 | RightCircleOutlined | RightCircleOutline |
|
||||
|
||||
### 编辑类图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 编辑 | EditOutlined | EditSOutline |
|
||||
| 删除 | DeleteOutlined | DeleteOutline |
|
||||
| 复制 | CopyOutlined | CopyOutline |
|
||||
| 剪切 | ScissorOutlined | ScissorOutline |
|
||||
| 撤销 | UndoOutlined | UndoOutline |
|
||||
| 重做 | RedoOutlined | RedoOutline |
|
||||
| 保存 | SaveOutlined | SaveOutline |
|
||||
|
||||
### 通信类图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 消息 | MessageOutlined | MessageOutline |
|
||||
| 邮件 | MailOutlined | MailOutline |
|
||||
| 电话 | PhoneOutlined | PhoneOutline |
|
||||
| 视频通话 | VideoCameraOutlined | VideoCameraOutline |
|
||||
| 语音 | AudioOutlined | AudioOutline |
|
||||
|
||||
### 媒体类图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 图片 | PictureOutlined | PictureOutline |
|
||||
| 视频 | VideoCameraOutlined | VideoCameraOutline |
|
||||
| 音频 | AudioOutlined | AudioOutline |
|
||||
| 文件 | FileOutlined | FileOutline |
|
||||
| 文件夹 | FolderOutlined | FolderOutline |
|
||||
| 相机 | CameraOutlined | CameraOutline |
|
||||
|
||||
### 导航类图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 菜单 | MenuOutlined | MenuOutline |
|
||||
| 汉堡菜单 | MenuFoldOutlined | MenuOutline |
|
||||
| 展开菜单 | MenuUnfoldOutlined | MenuOutline |
|
||||
| 面包屑 | BreadcrumbOutlined | BreadcrumbOutline |
|
||||
| 分页 | PaginationOutlined | PaginationOutline |
|
||||
|
||||
### 数据展示类图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 表格 | TableOutlined | TableOutline |
|
||||
| 列表 | UnorderedListOutlined | UnorderedListOutline |
|
||||
| 卡片 | CreditCardOutlined | CreditCardOutline |
|
||||
| 统计 | BarChartOutlined | BarChartOutline |
|
||||
| 饼图 | PieChartOutlined | PieChartOutline |
|
||||
| 折线图 | LineChartOutlined | LineChartOutline |
|
||||
| 仪表盘 | DashboardOutlined | DashboardOutline |
|
||||
|
||||
### 反馈类图标
|
||||
|
||||
| 功能描述 | PC端图标 | 移动端图标 |
|
||||
|---------|---------|-----------|
|
||||
| 成功 | CheckCircleOutlined | CheckCircleOutline |
|
||||
| 错误 | CloseCircleOutlined | CloseCircleOutline |
|
||||
| 警告 | ExclamationCircleOutlined | ExclamationCircleOutline |
|
||||
| 信息 | InfoCircleOutlined | InfoCircleOutline |
|
||||
| 加载 | LoadingOutlined | LoadingOutline |
|
||||
| 等待 | ClockCircleOutlined | ClockCircleOutline |
|
||||
|
||||
## 使用建议
|
||||
|
||||
### 1. 项目类型判断
|
||||
- **PC端项目**: 使用 `@ant-design/icons`
|
||||
- **移动端项目**: 使用 `antd-mobile-icons`
|
||||
|
||||
### 2. 图标选择原则
|
||||
- 优先选择语义化图标
|
||||
- 保持图标风格一致性
|
||||
- 考虑图标在不同尺寸下的清晰度
|
||||
|
||||
### 3. 常见错误避免
|
||||
- 不要混用PC端和移动端图标
|
||||
- 注意图标名称的大小写
|
||||
- 确保图标在对应包中存在
|
||||
|
||||
### 4. 图标替换策略
|
||||
当某个图标在目标包中不存在时:
|
||||
1. 查找语义相近的图标
|
||||
2. 使用通用图标(如QuestionOutlined)
|
||||
3. 考虑使用文字替代
|
||||
4. 创建自定义图标组件
|
||||
|
||||
## 实际项目中的使用示例
|
||||
|
||||
### PC端项目示例
|
||||
```typescript
|
||||
import {
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
QrcodeOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
UpOutlined,
|
||||
DownOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
CloseOutlined,
|
||||
CheckOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
```
|
||||
|
||||
### 移动端项目示例
|
||||
```typescript
|
||||
import {
|
||||
HomeOutline,
|
||||
UserOutline,
|
||||
SettingOutline,
|
||||
SearchOutline,
|
||||
AddOutline,
|
||||
EditSOutline,
|
||||
DeleteOutline,
|
||||
CopyOutline,
|
||||
RefreshOutline,
|
||||
QrCodeOutline,
|
||||
CalendarOutline,
|
||||
ClockCircleOutline,
|
||||
UpOutline,
|
||||
DownOutline,
|
||||
LeftOutline,
|
||||
RightOutline,
|
||||
CloseOutline,
|
||||
CheckOutline,
|
||||
ExclamationCircleOutline,
|
||||
InfoCircleOutline,
|
||||
CheckCircleOutline,
|
||||
CloseCircleOutline,
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **包依赖**: 确保项目中已安装对应的图标包
|
||||
2. **版本兼容**: 注意图标包版本与UI框架版本的兼容性
|
||||
3. **性能考虑**: 按需导入图标,避免全量导入
|
||||
4. **样式覆盖**: 可以通过CSS自定义图标颜色和大小
|
||||
5. **无障碍**: 为图标添加适当的aria-label属性
|
||||
|
||||
## 更新记录
|
||||
|
||||
- 2024-01-XX: 初始版本,包含常用图标对照
|
||||
- 后续根据实际使用情况持续更新
|
||||
271
nkebao/ICON_QUICK_REFERENCE.md
Normal file
271
nkebao/ICON_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# 图标快速参考表
|
||||
|
||||
## 快速查找
|
||||
|
||||
### 🔍 按功能查找
|
||||
|
||||
| 功能 | PC端 | 移动端 | 导入方式 |
|
||||
|------|------|--------|----------|
|
||||
| **添加** | PlusOutlined | AddOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **编辑** | EditOutlined | EditSOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **删除** | DeleteOutlined | DeleteOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **复制** | CopyOutlined | CopyOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **搜索** | SearchOutlined | SearchOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **刷新** | ReloadOutlined | RefreshOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **设置** | SettingOutlined | SettingOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **用户** | UserOutlined | UserOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **首页** | HomeOutlined | HomeOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **返回** | ArrowLeftOutlined | LeftOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **关闭** | CloseOutlined | CloseOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **确认** | CheckOutlined | CheckOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **成功** | CheckCircleOutlined | CheckCircleOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **错误** | CloseCircleOutlined | CloseCircleOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **警告** | ExclamationCircleOutlined | ExclamationCircleOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **信息** | InfoCircleOutlined | InfoCircleOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **加载** | LoadingOutlined | LoadingOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **向上** | UpOutlined | UpOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **向下** | DownOutlined | DownOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **向左** | LeftOutlined | LeftOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **向右** | RightOutlined | RightOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **消息** | MessageOutlined | MessageOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **电话** | PhoneOutlined | PhoneOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **日历** | CalendarOutlined | CalendarOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **时钟** | ClockCircleOutlined | ClockCircleOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **图片** | PictureOutlined | PictureOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **文件** | FileOutlined | FileOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **相机** | CameraOutlined | CameraOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **二维码** | QrcodeOutlined | QrCodeOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **微信** | WechatOutlined | WechatOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **设备** | MobileOutlined | MobileOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **团队** | TeamOutlined | TeamOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **订单** | ShoppingOutlined | ShoppingOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
| **支付** | PayCircleOutlined | PayCircleOutline | `@ant-design/icons` / `antd-mobile-icons` |
|
||||
|
||||
### 🔄 常见替换
|
||||
|
||||
| 原图标 | 替换为 | 说明 |
|
||||
|--------|--------|------|
|
||||
| RiseOutlined | UpOutline | 上升趋势 |
|
||||
| ThumbsUpOutlined | LikeOutline | 点赞功能 |
|
||||
| ShareAltOutlined | LinkOutline | 分享功能 |
|
||||
| BarChartOutlined | PieOutline | 图表功能 |
|
||||
| LineChartOutlined | PieOutline | 图表功能 |
|
||||
| UserAddOutlined | UserOutline | 用户添加 |
|
||||
| SettingOutline | SettingOutline | 设置(移动端) |
|
||||
|
||||
## 导入模板
|
||||
|
||||
### PC端项目模板
|
||||
```typescript
|
||||
import {
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
CloseOutlined,
|
||||
CheckOutlined,
|
||||
UpOutlined,
|
||||
DownOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
MessageOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
PictureOutlined,
|
||||
FileOutlined,
|
||||
CameraOutlined,
|
||||
QrcodeOutlined,
|
||||
WechatOutlined,
|
||||
MobileOutlined,
|
||||
TeamOutlined,
|
||||
ShoppingOutlined,
|
||||
PayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
```
|
||||
|
||||
### 移动端项目模板
|
||||
```typescript
|
||||
import {
|
||||
HomeOutline,
|
||||
UserOutline,
|
||||
SettingOutline,
|
||||
SearchOutline,
|
||||
AddOutline,
|
||||
EditSOutline,
|
||||
DeleteOutline,
|
||||
CopyOutline,
|
||||
RefreshOutline,
|
||||
CloseOutline,
|
||||
CheckOutline,
|
||||
UpOutline,
|
||||
DownOutline,
|
||||
LeftOutline,
|
||||
RightOutline,
|
||||
MessageOutline,
|
||||
CalendarOutline,
|
||||
ClockCircleOutline,
|
||||
PictureOutline,
|
||||
FileOutline,
|
||||
CameraOutline,
|
||||
QrCodeOutline,
|
||||
WechatOutline,
|
||||
MobileOutline,
|
||||
TeamOutline,
|
||||
ShoppingOutline,
|
||||
PayCircleOutline,
|
||||
CheckCircleOutline,
|
||||
CloseCircleOutline,
|
||||
ExclamationCircleOutline,
|
||||
InfoCircleOutline,
|
||||
LoadingOutline,
|
||||
} from 'antd-mobile-icons';
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础使用
|
||||
```typescript
|
||||
// PC端
|
||||
import { HomeOutlined, UserOutlined } from '@ant-design/icons';
|
||||
|
||||
<HomeOutlined style={{ fontSize: 16, color: '#1890ff' }} />
|
||||
<UserOutlined style={{ fontSize: 16, color: '#52c41a' }} />
|
||||
|
||||
// 移动端
|
||||
import { HomeOutline, UserOutline } from 'antd-mobile-icons';
|
||||
|
||||
<HomeOutline style={{ fontSize: 16, color: '#1890ff' }} />
|
||||
<UserOutline style={{ fontSize: 16, color: '#52c41a' }} />
|
||||
```
|
||||
|
||||
### 按钮中使用
|
||||
```typescript
|
||||
// PC端
|
||||
import { PlusOutlined, EditOutlined } from '@ant-design/icons';
|
||||
|
||||
<Button icon={<PlusOutlined />}>添加</Button>
|
||||
<Button icon={<EditOutlined />}>编辑</Button>
|
||||
|
||||
// 移动端
|
||||
import { AddOutline, EditSOutline } from 'antd-mobile-icons';
|
||||
|
||||
<Button>
|
||||
<AddOutline /> 添加
|
||||
</Button>
|
||||
<Button>
|
||||
<EditSOutline /> 编辑
|
||||
</Button>
|
||||
```
|
||||
|
||||
### 列表中使用
|
||||
```typescript
|
||||
// PC端
|
||||
import { DeleteOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
<Button icon={<DeleteOutlined />} danger>删除</Button>
|
||||
<Button icon={<CopyOutlined />}>复制</Button>
|
||||
|
||||
// 移动端
|
||||
import { DeleteOutline, CopyOutline } from 'antd-mobile-icons';
|
||||
|
||||
<Button color="danger">
|
||||
<DeleteOutline /> 删除
|
||||
</Button>
|
||||
<Button>
|
||||
<CopyOutline /> 复制
|
||||
</Button>
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
### ❌ 错误示例
|
||||
```typescript
|
||||
// 错误:混用PC端和移动端图标
|
||||
import { HomeOutlined } from '@ant-design/icons'; // PC端
|
||||
import { UserOutline } from 'antd-mobile-icons'; // 移动端
|
||||
|
||||
// 错误:使用不存在的图标
|
||||
import { RiseOutlined } from 'antd-mobile-icons'; // 不存在
|
||||
import { UserAddOutline } from 'antd-mobile-icons'; // 不存在
|
||||
```
|
||||
|
||||
### ✅ 正确示例
|
||||
```typescript
|
||||
// 正确:统一使用移动端图标
|
||||
import {
|
||||
HomeOutline,
|
||||
UserOutline,
|
||||
UpOutline, // 替换RiseOutlined
|
||||
UserOutline // 替换UserAddOutline
|
||||
} from 'antd-mobile-icons';
|
||||
|
||||
// 正确:统一使用PC端图标
|
||||
import {
|
||||
HomeOutlined,
|
||||
UserOutlined,
|
||||
RiseOutlined, // PC端存在
|
||||
UserAddOutlined // PC端存在
|
||||
} from '@ant-design/icons';
|
||||
```
|
||||
|
||||
## 快速检查清单
|
||||
|
||||
### 开发前检查
|
||||
- [ ] 确认项目类型(PC端/移动端)
|
||||
- [ ] 选择对应的图标包
|
||||
- [ ] 检查图标是否存在
|
||||
- [ ] 准备替换方案
|
||||
|
||||
### 开发中检查
|
||||
- [ ] 使用正确的导入方式
|
||||
- [ ] 图标名称大小写正确
|
||||
- [ ] 避免混用不同包的图标
|
||||
- [ ] 为图标添加适当的样式
|
||||
|
||||
### 开发后检查
|
||||
- [ ] 图标显示正常
|
||||
- [ ] 样式符合设计要求
|
||||
- [ ] 无障碍属性完整
|
||||
- [ ] 性能影响最小
|
||||
|
||||
## 紧急替换方案
|
||||
|
||||
当遇到图标不存在时,使用以下通用图标:
|
||||
|
||||
```typescript
|
||||
// 移动端通用图标
|
||||
import {
|
||||
QuestionCircleOutline, // 通用问号
|
||||
AppOutline, // 通用应用
|
||||
ToolOutline, // 通用工具
|
||||
SettingOutline, // 通用设置
|
||||
UserOutline, // 通用用户
|
||||
} from 'antd-mobile-icons';
|
||||
|
||||
// PC端通用图标
|
||||
import {
|
||||
QuestionCircleOutlined, // 通用问号
|
||||
AppstoreOutlined, // 通用应用
|
||||
ToolOutlined, // 通用工具
|
||||
SettingOutlined, // 通用设置
|
||||
UserOutlined, // 通用用户
|
||||
} from '@ant-design/icons';
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
- 2024-01-XX: 初始版本
|
||||
- 添加常用图标对照
|
||||
- 添加错误示例和正确示例
|
||||
- 添加快速检查清单
|
||||
- 添加紧急替换方案
|
||||
13
nkebao/index.html
Normal file
13
nkebao/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nkebao Base</title>
|
||||
<style>html{font-size:16px;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
6402
nkebao/package-lock.json
generated
Normal file
6402
nkebao/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
nkebao/package.json
Normal file
42
nkebao/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "nkebao-base",
|
||||
"license": "MIT",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"antd": "^5.13.1",
|
||||
"antd-mobile": "^5.39.1",
|
||||
"axios": "^1.6.7",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.14",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-pxtorem": "^6.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.75.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^7.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix"
|
||||
}
|
||||
}
|
||||
8
nkebao/postcss.config.js
Normal file
8
nkebao/postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-pxtorem': {
|
||||
rootValue: 16,
|
||||
propList: ['*'],
|
||||
},
|
||||
},
|
||||
};
|
||||
11
nkebao/src/App.tsx
Normal file
11
nkebao/src/App.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import AppRouter from "@/router";
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<AppRouter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
78
nkebao/src/api/request.ts
Normal file
78
nkebao/src/api/request.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, Method, AxiosResponse } from 'axios';
|
||||
import { Toast } from 'antd-mobile';
|
||||
|
||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||
const debounceMap = new Map<string, number>();
|
||||
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
instance.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(res: AxiosResponse) => {
|
||||
const { code, success, msg } = res.data || {};
|
||||
if (code === 200 || success) {
|
||||
return res.data.data ?? res.data;
|
||||
}
|
||||
Toast.show({ content: msg || '接口错误', position: 'top' });
|
||||
if (code === 401) {
|
||||
localStorage.removeItem('token');
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
if (currentPath === '/login') {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
||||
}
|
||||
}
|
||||
return Promise.reject(msg || '接口错误');
|
||||
},
|
||||
err => {
|
||||
Toast.show({ content: err.message || '网络异常', position: 'top' });
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
export function request(
|
||||
url: string,
|
||||
data?: any,
|
||||
method: Method = 'GET',
|
||||
config?: AxiosRequestConfig,
|
||||
debounceGap?: number
|
||||
): Promise<any> {
|
||||
const gap = typeof debounceGap === 'number' ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||
const now = Date.now();
|
||||
const last = debounceMap.get(key) || 0;
|
||||
if (gap > 0 && now - last < gap) {
|
||||
Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||
return Promise.reject('请求过于频繁,请稍后再试');
|
||||
}
|
||||
debounceMap.set(key, now);
|
||||
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
url,
|
||||
method,
|
||||
...config,
|
||||
};
|
||||
if (method.toUpperCase() === 'GET') {
|
||||
axiosConfig.params = data;
|
||||
} else {
|
||||
axiosConfig.data = data;
|
||||
}
|
||||
return instance(axiosConfig);
|
||||
}
|
||||
|
||||
export default request;
|
||||
87
nkebao/src/components/InfiniteList/InfiniteList.module.scss
Normal file
87
nkebao/src/components/InfiniteList/InfiniteList.module.scss
Normal file
@@ -0,0 +1,87 @@
|
||||
.listContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loadMoreButtonContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.noMoreText {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.pullToRefresh {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// 自定义滚动条样式
|
||||
.listContainer::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.listContainer::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.listContainer::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.listContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.listContainer {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.loadMoreButtonContainer {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.noMoreText {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
195
nkebao/src/components/InfiniteList/InfiniteList.tsx
Normal file
195
nkebao/src/components/InfiniteList/InfiniteList.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
PullToRefresh,
|
||||
InfiniteScroll,
|
||||
Button,
|
||||
SpinLoading,
|
||||
} from "antd-mobile";
|
||||
import styles from "./InfiniteList.module.scss";
|
||||
|
||||
interface InfiniteListProps<T> {
|
||||
// 数据相关
|
||||
data: T[];
|
||||
loading?: boolean;
|
||||
hasMore?: boolean;
|
||||
loadingText?: string;
|
||||
noMoreText?: string;
|
||||
|
||||
// 渲染相关
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
keyExtractor?: (item: T, index: number) => string | number;
|
||||
|
||||
// 事件回调
|
||||
onLoadMore?: () => Promise<void> | void;
|
||||
onRefresh?: () => Promise<void> | void;
|
||||
|
||||
// 样式相关
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
|
||||
// 功能开关
|
||||
enablePullToRefresh?: boolean;
|
||||
enableInfiniteScroll?: boolean;
|
||||
enableLoadMoreButton?: boolean;
|
||||
|
||||
// 自定义高度
|
||||
height?: string | number;
|
||||
minHeight?: string | number;
|
||||
}
|
||||
|
||||
const InfiniteList = <T extends any>({
|
||||
data,
|
||||
loading = false,
|
||||
hasMore = true,
|
||||
loadingText = "加载中...",
|
||||
noMoreText = "没有更多了",
|
||||
|
||||
renderItem,
|
||||
keyExtractor = (_, index) => index,
|
||||
|
||||
onLoadMore,
|
||||
onRefresh,
|
||||
|
||||
className = "",
|
||||
itemClassName = "",
|
||||
containerStyle = {},
|
||||
|
||||
enablePullToRefresh = true,
|
||||
enableInfiniteScroll = true,
|
||||
enableLoadMoreButton = false,
|
||||
|
||||
height = "100%",
|
||||
minHeight = "200px",
|
||||
}: InfiniteListProps<T>) => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理下拉刷新
|
||||
const handleRefresh = useCallback(async () => {
|
||||
if (!onRefresh) return;
|
||||
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await onRefresh();
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
// 处理加载更多
|
||||
const handleLoadMore = useCallback(async () => {
|
||||
if (!onLoadMore || loadingMore || !hasMore) return;
|
||||
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
await onLoadMore();
|
||||
} catch (error) {
|
||||
console.error("Load more failed:", error);
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [onLoadMore, loadingMore, hasMore]);
|
||||
|
||||
// 点击加载更多按钮
|
||||
const handleLoadMoreClick = useCallback(() => {
|
||||
handleLoadMore();
|
||||
}, [handleLoadMore]);
|
||||
|
||||
// 容器样式
|
||||
const containerStyles: React.CSSProperties = {
|
||||
height,
|
||||
minHeight,
|
||||
...containerStyle,
|
||||
};
|
||||
|
||||
// 渲染列表项
|
||||
const renderListItems = () => {
|
||||
return data.map((item, index) => (
|
||||
<div
|
||||
key={keyExtractor(item, index)}
|
||||
className={`${styles.listItem} ${itemClassName}`}
|
||||
>
|
||||
{renderItem(item, index)}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
// 渲染加载更多按钮
|
||||
const renderLoadMoreButton = () => {
|
||||
if (!enableLoadMoreButton || !hasMore) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.loadMoreButtonContainer}>
|
||||
<Button
|
||||
size="small"
|
||||
loading={loadingMore}
|
||||
onClick={handleLoadMoreClick}
|
||||
disabled={loading || !hasMore}
|
||||
>
|
||||
{loadingMore ? loadingText : "点击加载更多"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染无更多数据提示
|
||||
const renderNoMoreText = () => {
|
||||
if (hasMore || data.length === 0) return null;
|
||||
|
||||
return <div className={styles.noMoreText}>{noMoreText}</div>;
|
||||
};
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmptyState = () => {
|
||||
if (data.length > 0 || loading) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>📝</div>
|
||||
<div className={styles.emptyText}>暂无数据</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={`${styles.listContainer} ${className}`}
|
||||
style={containerStyles}
|
||||
>
|
||||
{renderListItems()}
|
||||
{renderLoadMoreButton()}
|
||||
{renderNoMoreText()}
|
||||
{renderEmptyState()}
|
||||
|
||||
{/* 无限滚动组件 */}
|
||||
{enableInfiniteScroll && (
|
||||
<InfiniteScroll
|
||||
loadMore={handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
threshold={100}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 如果启用下拉刷新,包装PullToRefresh
|
||||
if (enablePullToRefresh && onRefresh) {
|
||||
return (
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
className={styles.pullToRefresh}
|
||||
>
|
||||
{content}
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export default InfiniteList;
|
||||
36
nkebao/src/components/Layout/Layout.tsx
Normal file
36
nkebao/src/components/Layout/Layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { SpinLoading } from "antd-mobile";
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
interface LayoutProps {
|
||||
loading?: boolean;
|
||||
children?: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({
|
||||
children,
|
||||
header,
|
||||
footer,
|
||||
loading = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{header && <header>{header}</header>}
|
||||
<main>
|
||||
{loading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
<div className={styles.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</main>
|
||||
{footer && <footer>{footer}</footer>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
28
nkebao/src/components/Layout/layout.module.scss
Normal file
28
nkebao/src/components/Layout/layout.module.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
.container main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
margin-top: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
53
nkebao/src/components/LineChart.tsx
Normal file
53
nkebao/src/components/LineChart.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import ReactECharts from "echarts-for-react";
|
||||
|
||||
interface LineChartProps {
|
||||
title?: string;
|
||||
xData: string[];
|
||||
yData: number[];
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const LineChart: React.FC<LineChartProps> = ({
|
||||
title = "",
|
||||
xData,
|
||||
yData,
|
||||
height = 200,
|
||||
}) => {
|
||||
const option = {
|
||||
title: {
|
||||
text: title,
|
||||
left: "center",
|
||||
textStyle: { fontSize: 16 },
|
||||
},
|
||||
tooltip: { trigger: "axis" },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: xData,
|
||||
boundaryGap: false,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
boundaryGap: ["10%", "10%"], // 上下留白
|
||||
min: (value: any) => value.min - 10, // 下方多留一点空间
|
||||
max: (value: any) => value.max + 10, // 上方多留一点空间
|
||||
minInterval: 1,
|
||||
axisLabel: { margin: 12 },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: yData,
|
||||
type: "line",
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
lineStyle: { color: "#1677ff" },
|
||||
itemStyle: { color: "#1677ff" },
|
||||
},
|
||||
],
|
||||
grid: { left: 40, right: 24, top: 40, bottom: 32 },
|
||||
};
|
||||
|
||||
return <ReactECharts option={option} style={{ height, width: "100%" }} />;
|
||||
};
|
||||
|
||||
export default LineChart;
|
||||
77
nkebao/src/components/MeauMobile/MeauMoible.tsx
Normal file
77
nkebao/src/components/MeauMobile/MeauMoible.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { TabBar } from "antd-mobile";
|
||||
import { PieOutline, UserOutline } from "antd-mobile-icons";
|
||||
import { HomeOutlined, TeamOutlined } from "@ant-design/icons";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: "home",
|
||||
title: "首页",
|
||||
icon: <HomeOutlined />,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
key: "scene",
|
||||
title: "场景获客",
|
||||
icon: <TeamOutlined />,
|
||||
path: "/scenarios",
|
||||
},
|
||||
{
|
||||
key: "work",
|
||||
title: "工作台",
|
||||
icon: <PieOutline />,
|
||||
path: "/workspace",
|
||||
},
|
||||
{
|
||||
key: "mine",
|
||||
title: "我的",
|
||||
icon: <UserOutline />,
|
||||
path: "/mine",
|
||||
},
|
||||
];
|
||||
|
||||
// 需要展示菜单的路由白名单(可根据实际业务调整)
|
||||
const menuPaths = ["/", "/scenarios", "/workspace", "/mine"];
|
||||
|
||||
const MeauMobile: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [activeKey, setActiveKey] = useState("home");
|
||||
|
||||
// 根据当前路由自动设置 activeKey,支持嵌套路由
|
||||
useEffect(() => {
|
||||
const found = tabs.find((tab) =>
|
||||
tab.path === "/"
|
||||
? location.pathname === "/"
|
||||
: location.pathname.startsWith(tab.path)
|
||||
);
|
||||
if (found) setActiveKey(found.key);
|
||||
}, [location.pathname]);
|
||||
|
||||
// 判断当前路由是否需要展示菜单
|
||||
const showMenu = menuPaths.some((path) =>
|
||||
path === "/"
|
||||
? location.pathname === "/"
|
||||
: location.pathname.startsWith(path)
|
||||
);
|
||||
if (!showMenu) return null;
|
||||
|
||||
return (
|
||||
<TabBar
|
||||
style={{ background: "#fff" }}
|
||||
activeKey={activeKey}
|
||||
onChange={(key) => {
|
||||
setActiveKey(key);
|
||||
const tab = tabs.find((t) => t.key === key);
|
||||
if (tab && tab.path) navigate(tab.path);
|
||||
}}
|
||||
>
|
||||
{tabs.map((item) => (
|
||||
<TabBar.Item key={item.key} icon={item.icon} title={item.title} />
|
||||
))}
|
||||
</TabBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeauMobile;
|
||||
56
nkebao/src/components/PlaceholderPage.tsx
Normal file
56
nkebao/src/components/PlaceholderPage.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
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";
|
||||
|
||||
interface PlaceholderPageProps {
|
||||
title: string;
|
||||
showBack?: boolean;
|
||||
showAddButton?: boolean;
|
||||
addButtonText?: string;
|
||||
showFooter?: boolean;
|
||||
}
|
||||
|
||||
const PlaceholderPage: React.FC<PlaceholderPageProps> = ({
|
||||
title,
|
||||
showBack = true,
|
||||
showAddButton = false,
|
||||
addButtonText = "新建",
|
||||
showFooter = true,
|
||||
}) => {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
backArrow={showBack}
|
||||
style={{ background: "#fff" }}
|
||||
onBack={showBack ? () => window.history.back() : undefined}
|
||||
left={
|
||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
showAddButton ? (
|
||||
<Button size="small" color="primary">
|
||||
<PlusOutlined />
|
||||
<span style={{ marginLeft: 4, fontSize: 12 }}>
|
||||
{addButtonText}
|
||||
</span>
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={showFooter ? <MeauMobile /> : undefined}
|
||||
>
|
||||
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
|
||||
<h3>{title}页面</h3>
|
||||
<p>此页面正在开发中...</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaceholderPage;
|
||||
7
nkebao/src/main.tsx
Normal file
7
nkebao/src/main.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles/global.scss";
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
root.render(<App />);
|
||||
14
nkebao/src/pages/contact-import/ContactImport.tsx
Normal file
14
nkebao/src/pages/contact-import/ContactImport.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const ContactImport: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage
|
||||
title="联系人导入"
|
||||
showAddButton
|
||||
addButtonText="导入联系人"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactImport;
|
||||
10
nkebao/src/pages/content/Content.tsx
Normal file
10
nkebao/src/pages/content/Content.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const Content: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="内容管理" showAddButton addButtonText="新建内容" />
|
||||
);
|
||||
};
|
||||
|
||||
export default Content;
|
||||
8
nkebao/src/pages/content/NewContent.tsx
Normal file
8
nkebao/src/pages/content/NewContent.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewContent: React.FC = () => {
|
||||
return <PlaceholderPage title="新建内容" />;
|
||||
};
|
||||
|
||||
export default NewContent;
|
||||
10
nkebao/src/pages/content/materials/List.tsx
Normal file
10
nkebao/src/pages/content/materials/List.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const Materials: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="素材管理" showAddButton addButtonText="新建素材" />
|
||||
);
|
||||
};
|
||||
|
||||
export default Materials;
|
||||
8
nkebao/src/pages/content/materials/New.tsx
Normal file
8
nkebao/src/pages/content/materials/New.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const MaterialsNew: React.FC = () => {
|
||||
return <PlaceholderPage title="新建素材" />;
|
||||
};
|
||||
|
||||
export default MaterialsNew;
|
||||
30
nkebao/src/pages/devices/DeviceDetail.tsx
Normal file
30
nkebao/src/pages/devices/DeviceDetail.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { NavBar } from "antd-mobile";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
const DeviceDetail: 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 DeviceDetail;
|
||||
37
nkebao/src/pages/devices/Devices.tsx
Normal file
37
nkebao/src/pages/devices/Devices.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { NavBar, Button } from "antd-mobile";
|
||||
import { AddOutline } from "antd-mobile-icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
const Devices: 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">
|
||||
<AddOutline />
|
||||
<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 Devices;
|
||||
31
nkebao/src/pages/home/api.ts
Normal file
31
nkebao/src/pages/home/api.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import request from '@/api/request';
|
||||
|
||||
// 设备统计
|
||||
export function getDeviceStats() {
|
||||
return request('/v1/dashboard/device-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 微信号统计
|
||||
export function getWechatStats() {
|
||||
return request('/v1/dashboard/wechat-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 今日数据统计
|
||||
export function getTodayStats() {
|
||||
return request('/v1/dashboard/today-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 首页仪表盘总览
|
||||
export function getDashboard() {
|
||||
return request('/v1/dashboard', {}, 'GET');
|
||||
}
|
||||
|
||||
// 获客场景统计
|
||||
export function getPlanStats(params:any) {
|
||||
return request('/v1/dashboard/plan-stats', params, 'GET');
|
||||
}
|
||||
|
||||
// 近七天统计
|
||||
export function getSevenDayStats() {
|
||||
return request('/v1/dashboard/sevenDay-stats', {}, 'GET');
|
||||
}
|
||||
360
nkebao/src/pages/home/index.module.scss
Normal file
360
nkebao/src/pages/home/index.module.scss
Normal file
@@ -0,0 +1,360 @@
|
||||
.home-page {
|
||||
padding: 12px;
|
||||
background: #f8f6f3;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 导航栏样式
|
||||
.nav-title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 2px 4px rgba(24, 142, 238, 0.2);
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.error-tip {
|
||||
font-size: 12px;
|
||||
color: #f97316;
|
||||
background: #fef3c7;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片网格
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
line-height: 1.2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
line-height: 1.2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #3b82f6;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
// Loading状态样式
|
||||
.stat-card {
|
||||
.stat-label:empty::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 60px;
|
||||
height: 12px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
span:empty::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
div:empty::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 通用区域样式
|
||||
.section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
position: relative;
|
||||
padding-left: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 场景统计网格
|
||||
.scene-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scene-item {
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.scene-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scene-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.scene-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.scene-label {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
// 今日数据网格
|
||||
.today-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.today-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.today-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.today-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.today-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
// 图表容器
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 375px) {
|
||||
.home-page {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 10px 6px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.scene-grid,
|
||||
.today-grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.scene-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.scene-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scene-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.today-value {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.today-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: 140px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
370
nkebao/src/pages/home/index.tsx
Normal file
370
nkebao/src/pages/home/index.tsx
Normal file
@@ -0,0 +1,370 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NavBar } from "antd-mobile";
|
||||
import {
|
||||
BellOutlined,
|
||||
MobileOutlined,
|
||||
UserOutlined,
|
||||
MessageOutlined,
|
||||
TeamOutlined,
|
||||
RiseOutlined,
|
||||
LineChartOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import LineChart from "@/components/LineChart";
|
||||
import {
|
||||
getPlanStats,
|
||||
getSevenDayStats,
|
||||
getTodayStats,
|
||||
getDashboard,
|
||||
} from "./api";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface DashboardData {
|
||||
deviceNum?: number;
|
||||
wechatNum?: number;
|
||||
aliveWechatNum?: number;
|
||||
}
|
||||
|
||||
interface TodayStatsData {
|
||||
momentsNum?: number;
|
||||
groupPushNum?: number;
|
||||
passRate?: string;
|
||||
sysActive?: string;
|
||||
}
|
||||
|
||||
interface SevenDayStatsData {
|
||||
date?: string[];
|
||||
allNum?: number[];
|
||||
}
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [sceneStats, setSceneStats] = useState<any[]>([]);
|
||||
const [todayStats, setTodayStats] = useState<any[]>([]);
|
||||
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] =
|
||||
await Promise.allSettled([
|
||||
getDashboard(),
|
||||
getPlanStats({ num: 4 }),
|
||||
getSevenDayStats(),
|
||||
getTodayStats(),
|
||||
]);
|
||||
|
||||
// 处理仪表板数据
|
||||
if (dashboardResult.status === "fulfilled") {
|
||||
setDashboard(dashboardResult.value);
|
||||
} else {
|
||||
console.warn("仪表板API失败:", dashboardResult.reason);
|
||||
setApiError("API连接异常,显示默认数据");
|
||||
}
|
||||
|
||||
// 处理计划统计数据
|
||||
if (planStatsResult.status === "fulfilled") {
|
||||
setSceneStats(planStatsResult.value);
|
||||
} else {
|
||||
console.warn("计划统计API失败:", planStatsResult.reason);
|
||||
}
|
||||
|
||||
// 处理七天统计数据
|
||||
if (sevenDayResult.status === "fulfilled") {
|
||||
setSevenDayStats(sevenDayResult.value);
|
||||
} else {
|
||||
console.warn("七天统计API失败:", sevenDayResult.reason);
|
||||
}
|
||||
|
||||
// 处理今日统计数据
|
||||
if (todayResult.status === "fulfilled") {
|
||||
const todayStatsData = [
|
||||
{
|
||||
label: "同步朋友圈",
|
||||
value: todayResult.value?.momentsNum || 0,
|
||||
icon: (
|
||||
<MessageOutlined style={{ fontSize: 16, color: "#8b5cf6" }} />
|
||||
),
|
||||
color: "#8b5cf6",
|
||||
},
|
||||
{
|
||||
label: "群发任务",
|
||||
value: todayResult.value?.groupPushNum || 0,
|
||||
icon: <TeamOutlined style={{ fontSize: 16, color: "#f97316" }} />,
|
||||
color: "#f97316",
|
||||
},
|
||||
{
|
||||
label: "获客转化率",
|
||||
value: todayResult.value?.passRate || "0%",
|
||||
icon: <RiseOutlined style={{ fontSize: 16, color: "#22c55e" }} />,
|
||||
color: "#22c55e",
|
||||
},
|
||||
{
|
||||
label: "系统活跃度",
|
||||
value: todayResult.value?.sysActive || "0%",
|
||||
icon: (
|
||||
<LineChartOutlined style={{ fontSize: 16, color: "#3b82f6" }} />
|
||||
),
|
||||
color: "#3b82f6",
|
||||
},
|
||||
];
|
||||
setTodayStats(todayStatsData);
|
||||
} else {
|
||||
console.warn("今日统计API失败:", todayResult.reason);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取数据失败:", error);
|
||||
setApiError(error instanceof Error ? error.message : "数据加载失败");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleDevicesClick = () => {
|
||||
navigate("/devices");
|
||||
};
|
||||
|
||||
const handleWechatClick = () => {
|
||||
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={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>
|
||||
<span className={style["nav-text"]}>存客宝</span>
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["home-page"]}>
|
||||
<div className={style["content-wrapper"]}>
|
||||
{/* 统计卡片 */}
|
||||
<div className={style["stats-grid"]}>
|
||||
<div className={style["stat-card"]} onClick={handleDevicesClick}>
|
||||
<div className={style["stat-label"]}>设备数量</div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span>{dashboard.deviceNum || 42}</span>
|
||||
<MobileOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["stat-card"]} onClick={handleWechatClick}>
|
||||
<div className={style["stat-label"]}>微信号数量</div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span>{dashboard.wechatNum || 42}</span>
|
||||
<TeamOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-label"]}>在线微信号</div>
|
||||
<div className={style["stat-value"]}>
|
||||
<span>{dashboard.aliveWechatNum || 35}</span>
|
||||
<LineChartOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||
</div>
|
||||
<div className={style["progress-bar"]}>
|
||||
<div
|
||||
className={style["progress-fill"]}
|
||||
style={{
|
||||
width: `${
|
||||
(dashboard.wechatNum || 0) > 0
|
||||
? ((dashboard.aliveWechatNum || 0) /
|
||||
(dashboard.wechatNum || 1)) *
|
||||
100
|
||||
: 0
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 场景获客统计 */}
|
||||
<div className={style["section"]}>
|
||||
<div className={style["section-header"]}>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 今日数据统计 */}
|
||||
<div className={style["section"]}>
|
||||
<div className={style["section-header"]}>
|
||||
<h2 className={style["section-title"]}>今日数据</h2>
|
||||
</div>
|
||||
<div className={style["today-grid"]}>
|
||||
{todayStatsData.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={style["today-item"]}
|
||||
onClick={() => stat.path && navigate(stat.path)}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 趋势图表 - 保持原有实现 */}
|
||||
<div className={style["section"]}>
|
||||
<div className={style["section-header"]}>
|
||||
<span className={style["section-title"]}>获客趋势</span>
|
||||
</div>
|
||||
<div className={style["chart-container"]}>
|
||||
<LineChart
|
||||
xData={sevenDayStats.date || []}
|
||||
yData={sevenDayStats.allNum || []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
53
nkebao/src/pages/login/api.ts
Normal file
53
nkebao/src/pages/login/api.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/api/request';
|
||||
export interface LoginParams {
|
||||
phone: string;
|
||||
password?: string;
|
||||
verificationCode?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: {
|
||||
token: string;
|
||||
token_expired: string;
|
||||
member: {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
s2_accountId: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface SendCodeResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
// 密码登录
|
||||
export function loginWithPassword(params:any) {
|
||||
return request('/v1/auth/login', params, 'POST');
|
||||
}
|
||||
|
||||
// 验证码登录
|
||||
export function loginWithCode(params:any) {
|
||||
return request('/v1/auth/login-code', params, 'POST');
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
export function sendVerificationCode(params:any) {
|
||||
return request('/v1/auth/code',params, 'POST');
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
export function logout() {
|
||||
return request('/v1/auth/logout', {}, 'POST');
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return request('/v1/auth/user-info', {}, 'GET');
|
||||
}
|
||||
439
nkebao/src/pages/login/login.module.scss
Normal file
439
nkebao/src/pages/login/login.module.scss
Normal file
@@ -0,0 +1,439 @@
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: var(--primary-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 背景装饰
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: -75px;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 50%;
|
||||
right: 10%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: #ffffff;
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 6px 12px var(--primary-shadow);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// 标签页样式
|
||||
.tab-container {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-radius: 7px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
background: white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
display: none; // 隐藏分割线指示器
|
||||
}
|
||||
|
||||
// 表单样式
|
||||
.login-form {
|
||||
:global(.adm-form) {
|
||||
--adm-font-size-main: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px var(--primary-shadow-light);
|
||||
}
|
||||
}
|
||||
|
||||
.input-prefix {
|
||||
padding: 0 12px;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-right: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.phone-input,
|
||||
.password-input,
|
||||
.code-input {
|
||||
flex: 1;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 12px 14px !important;
|
||||
font-size: 15px !important;
|
||||
color: #333 !important;
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.eye-icon {
|
||||
padding: 0 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.send-code-btn {
|
||||
padding: 6px 12px;
|
||||
margin-right: 6px;
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 8px var(--primary-shadow);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #e5e5e5;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.agreement-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
|
||||
:global(.adm-checkbox) {
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 46px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
box-shadow: 0 6px 12px var(--primary-shadow);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 16px var(--primary-shadow-dark);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #e5e5e5;
|
||||
color: #999;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin: 24px 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
span {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 0 12px;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.third-party-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.third-party-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-icon,
|
||||
.apple-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
background: #07c160;
|
||||
box-shadow: 0 3px 8px rgba(7, 193, 96, 0.3);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.apple-icon {
|
||||
background: #000;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 24px 20px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.third-party-login {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.third-party-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.wechat-icon,
|
||||
.apple-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
332
nkebao/src/pages/login/login.tsx
Normal file
332
nkebao/src/pages/login/login.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Form, Input, Button, Toast, Tabs, Checkbox } from "antd-mobile";
|
||||
import {
|
||||
EyeInvisibleOutline,
|
||||
EyeOutline,
|
||||
UserOutline,
|
||||
} from "antd-mobile-icons";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
|
||||
import style from "./login.module.scss";
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [activeTab, setActiveTab] = useState(1); // 1: 密码登录, 2: 验证码登录
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { login } = useUserStore();
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
// 检查URL是否为登录页面
|
||||
const isLoginPage = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
return urlObj.pathname === "/login" || urlObj.pathname.endsWith("/login");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 发送验证码
|
||||
const handleSendVerificationCode = async () => {
|
||||
const account = form.getFieldValue("account");
|
||||
|
||||
if (!account) {
|
||||
Toast.show({ content: "请输入手机号", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 手机号格式验证
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(account)) {
|
||||
Toast.show({ content: "请输入正确的11位手机号", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await sendVerificationCode({
|
||||
mobile: account,
|
||||
type: "login",
|
||||
});
|
||||
|
||||
Toast.show({ content: "验证码已发送", position: "top" });
|
||||
setCountdown(60);
|
||||
} catch (error) {
|
||||
// 错误已在request中处理,这里不需要额外处理
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async (values: any) => {
|
||||
if (!agreeToTerms) {
|
||||
Toast.show({ content: "请同意用户协议和隐私政策", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 添加typeId参数
|
||||
const loginParams = {
|
||||
...values,
|
||||
typeId: activeTab as number,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (activeTab === 1) {
|
||||
response = await loginWithPassword(loginParams);
|
||||
} else {
|
||||
response = await loginWithCode(loginParams);
|
||||
}
|
||||
console.log(response, "response");
|
||||
|
||||
// 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中)
|
||||
login(response.token, response.member);
|
||||
|
||||
Toast.show({ content: "登录成功", position: "top" });
|
||||
|
||||
// 跳转到首页或重定向URL
|
||||
const returnUrl = searchParams.get("returnUrl");
|
||||
if (returnUrl) {
|
||||
const decodedUrl = decodeURIComponent(returnUrl);
|
||||
if (isLoginPage(decodedUrl)) {
|
||||
navigate("/");
|
||||
} else {
|
||||
window.location.href = decodedUrl;
|
||||
}
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 错误已在request中处理,这里不需要额外处理
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 第三方登录处理
|
||||
const handleWechatLogin = () => {
|
||||
Toast.show({ content: "微信登录功能开发中", position: "top" });
|
||||
};
|
||||
|
||||
const handleAppleLogin = () => {
|
||||
Toast.show({ content: "Apple登录功能开发中", position: "top" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style["login-page"]}>
|
||||
{/* 背景装饰 */}
|
||||
<div className={style["bg-decoration"]}>
|
||||
<div className={style["bg-circle"]}></div>
|
||||
<div className={style["bg-circle"]}></div>
|
||||
<div className={style["bg-circle"]}></div>
|
||||
</div>
|
||||
|
||||
<div className={style["login-container"]}>
|
||||
{/* Logo和标题区域 */}
|
||||
<div className={style["login-header"]}>
|
||||
<div className={style["logo-section"]}>
|
||||
<div className={style["logo-icon"]}>
|
||||
<UserOutline />
|
||||
</div>
|
||||
<h1 className={style["app-name"]}>存客宝</h1>
|
||||
</div>
|
||||
<p className={style["subtitle"]}>登录您的账户继续使用</p>
|
||||
</div>
|
||||
|
||||
{/* 登录表单 */}
|
||||
<div className={style["form-container"]}>
|
||||
{/* 标签页切换 */}
|
||||
<div className={style["tab-container"]}>
|
||||
<div
|
||||
className={`${style["tab-item"]} ${
|
||||
activeTab === 1 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab(1)}
|
||||
>
|
||||
密码登录
|
||||
</div>
|
||||
<div
|
||||
className={`${style["tab-item"]} ${
|
||||
activeTab === 2 ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab(2)}
|
||||
>
|
||||
验证码登录
|
||||
</div>
|
||||
<div
|
||||
className={`${style["tab-indicator"]} ${
|
||||
activeTab === 2 ? style["slide"] : ""
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className={style["login-form"]}
|
||||
onFinish={handleLogin}
|
||||
>
|
||||
{/* 手机号输入 */}
|
||||
<Form.Item
|
||||
name="account"
|
||||
label="手机号"
|
||||
rules={[
|
||||
{ required: true, message: "请输入手机号" },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: "请输入正确的11位手机号",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className={style["input-wrapper"]}>
|
||||
<span className={style["input-prefix"]}>+86</span>
|
||||
<Input
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
className={style["phone-input"]}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* 密码输入 */}
|
||||
{activeTab === 1 && (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
>
|
||||
<div className={style["input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="请输入密码"
|
||||
clearable
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={style["password-input"]}
|
||||
/>
|
||||
<div
|
||||
className={style["eye-icon"]}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOutline /> : <EyeInvisibleOutline />}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 验证码输入 */}
|
||||
{activeTab === 2 && (
|
||||
<Form.Item
|
||||
name="verificationCode"
|
||||
label="验证码"
|
||||
rules={[{ required: true, message: "请输入验证码" }]}
|
||||
>
|
||||
<div className={style["input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="请输入验证码"
|
||||
clearable
|
||||
className={style["code-input"]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`${style["send-code-btn"]} ${
|
||||
countdown > 0 ? style["disabled"] : ""
|
||||
}`}
|
||||
onClick={handleSendVerificationCode}
|
||||
disabled={loading || countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : "获取验证码"}
|
||||
</button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 用户协议 */}
|
||||
<div className={style["agreement-section"]}>
|
||||
<Checkbox
|
||||
checked={agreeToTerms}
|
||||
onChange={setAgreeToTerms}
|
||||
className={style["agreement-checkbox"]}
|
||||
>
|
||||
<span className={style["agreement-text"]}>
|
||||
我已阅读并同意
|
||||
<span className={style["agreement-link"]}>
|
||||
《存客宝用户协议》
|
||||
</span>
|
||||
和
|
||||
<span className={style["agreement-link"]}>《隐私政策》</span>
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<Button
|
||||
block
|
||||
type="submit"
|
||||
color="primary"
|
||||
loading={loading}
|
||||
size="large"
|
||||
className={style["login-btn"]}
|
||||
>
|
||||
{loading ? "登录中..." : "登录"}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className={style["divider"]}>
|
||||
<span>其他登录方式</span>
|
||||
</div>
|
||||
|
||||
{/* 第三方登录 */}
|
||||
<div className={style["third-party-login"]}>
|
||||
<div
|
||||
className={style["third-party-item"]}
|
||||
onClick={handleWechatLogin}
|
||||
>
|
||||
<div className={style["wechat-icon"]}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
width="24"
|
||||
className={style["wechat-icon"]}
|
||||
>
|
||||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.81-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.595-6.348zM5.959 5.48c.609 0 1.104.498 1.104 1.112 0 .612-.495 1.11-1.104 1.11-.612 0-1.108-.498-1.108-1.11 0-.614.496-1.112 1.108-1.112zm5.315 0c.61 0 1.107.498 1.107 1.112 0 .612-.497 1.11-1.107 1.11-.611 0-1.105-.498-1.105-1.11 0-.614.494-1.112 1.105-1.112z"></path>
|
||||
<path d="M23.002 15.816c0-3.309-3.136-6-7-6-3.863 0-7 2.691-7 6 0 3.31 3.137 6 7 6 .814 0 1.601-.099 2.338-.285a.7.7 0 0 1 .579.08l1.5.87a.267.267 0 0 0 .135.044c.13 0 .236-.108.236-.241 0-.06-.023-.118-.038-.17l-.309-1.167a.476.476 0 0 1 .172-.534c1.645-1.17 2.387-2.835 2.387-4.597zm-9.498-1.19c-.497 0-.9-.407-.9-.908a.905.905 0 0 1 .9-.91c.498 0 .9.408.9.91 0 .5-.402.908-.9.908zm4.998 0c-.497 0-.9-.407-.9-.908a.905.905 0 0 1 .9-.91c.498 0 .9.408.9.91 0 .5-.402.908-.9.908z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span>微信</span>
|
||||
</div>
|
||||
<div
|
||||
className={style["third-party-item"]}
|
||||
onClick={handleAppleLogin}
|
||||
>
|
||||
<div className={style["apple-icon"]}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>Apple</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
14
nkebao/src/pages/mine/api.ts
Normal file
14
nkebao/src/pages/mine/api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import request from '@/api/request';
|
||||
|
||||
// 设备统计
|
||||
export function getDeviceStats() {
|
||||
return request('/v1/dashboard/device-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 微信号统计
|
||||
export function getWechatStats() {
|
||||
return request('/v1/dashboard/wechat-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 你可以根据需要继续添加其他接口
|
||||
// 例如:场景获客统计、今日数据统计等
|
||||
150
nkebao/src/pages/mine/index.module.scss
Normal file
150
nkebao/src/pages/mine/index.module.scss
Normal file
@@ -0,0 +1,150 @@
|
||||
.mine-page {
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.adm-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--primary-color);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
font-size: 14px;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-points {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.adm-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.adm-list-item) {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-content-prefix) {
|
||||
margin-right: 12px;
|
||||
color: var(--primary-color);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-content-main) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-title) {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-description) {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-content-arrow) {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
border-radius: 8px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 375px) {
|
||||
.mine-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
:global(.adm-list-item) {
|
||||
padding: 12px;
|
||||
|
||||
:global(.adm-list-item-content-prefix) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:global(.adm-list-item-title) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
nkebao/src/pages/mine/index.tsx
Normal file
143
nkebao/src/pages/mine/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React from "react";
|
||||
import { Card, NavBar, List, Button } from "antd-mobile";
|
||||
import {
|
||||
UserOutline,
|
||||
AppOutline,
|
||||
BellOutline,
|
||||
HeartOutline,
|
||||
StarOutline,
|
||||
MessageOutline,
|
||||
SendOutline,
|
||||
MailOutline,
|
||||
} from "antd-mobile-icons";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
const Mine: React.FC = () => {
|
||||
const userInfo = {
|
||||
name: "张三",
|
||||
avatar: "https://via.placeholder.com/60",
|
||||
level: "VIP会员",
|
||||
points: 1280,
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
icon: <UserOutline />,
|
||||
title: "个人资料",
|
||||
subtitle: "修改个人信息",
|
||||
path: "/profile",
|
||||
},
|
||||
{
|
||||
icon: <AppOutline />,
|
||||
title: "系统设置",
|
||||
subtitle: "应用设置与偏好",
|
||||
path: "/settings",
|
||||
},
|
||||
{
|
||||
icon: <BellOutline />,
|
||||
title: "消息通知",
|
||||
subtitle: "通知设置",
|
||||
path: "/notifications",
|
||||
},
|
||||
{
|
||||
icon: <HeartOutline />,
|
||||
title: "我的收藏",
|
||||
subtitle: "收藏的内容",
|
||||
path: "/favorites",
|
||||
},
|
||||
{
|
||||
icon: <StarOutline />,
|
||||
title: "我的评价",
|
||||
subtitle: "查看评价记录",
|
||||
path: "/reviews",
|
||||
},
|
||||
{
|
||||
icon: <MessageOutline />,
|
||||
title: "意见反馈",
|
||||
subtitle: "问题反馈与建议",
|
||||
path: "/feedback",
|
||||
},
|
||||
{
|
||||
icon: <SendOutline />,
|
||||
title: "联系客服",
|
||||
subtitle: "在线客服",
|
||||
path: "/customer-service",
|
||||
},
|
||||
{
|
||||
icon: <MailOutline />,
|
||||
title: "关于我们",
|
||||
subtitle: "版本信息",
|
||||
path: "/about",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
我的
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["mine-page"]}>
|
||||
{/* 用户信息卡片 */}
|
||||
<Card className={style["user-card"]}>
|
||||
<div className={style["user-info"]}>
|
||||
<div className={style["user-avatar"]}>
|
||||
<img src={userInfo.avatar} alt="头像" />
|
||||
</div>
|
||||
<div className={style["user-details"]}>
|
||||
<div className={style["user-name"]}>{userInfo.name}</div>
|
||||
<div className={style["user-level"]}>{userInfo.level}</div>
|
||||
<div className={style["user-points"]}>
|
||||
积分: {userInfo.points}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 菜单列表 */}
|
||||
<Card className={style["menu-card"]}>
|
||||
<List>
|
||||
{menuItems.map((item, index) => (
|
||||
<List.Item
|
||||
key={index}
|
||||
prefix={item.icon}
|
||||
title={item.title}
|
||||
description={item.subtitle}
|
||||
arrow
|
||||
onClick={() => {
|
||||
// 这里可以添加导航逻辑
|
||||
console.log(`点击了: ${item.title}`);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
|
||||
{/* 退出登录按钮 */}
|
||||
<div className={style["logout-section"]}>
|
||||
<Button
|
||||
block
|
||||
color="danger"
|
||||
fill="outline"
|
||||
className={style["logout-btn"]}
|
||||
onClick={() => {
|
||||
// 这里可以添加退出登录逻辑
|
||||
console.log("退出登录");
|
||||
}}
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mine;
|
||||
8
nkebao/src/pages/orders/Orders.tsx
Normal file
8
nkebao/src/pages/orders/Orders.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const Orders: React.FC = () => {
|
||||
return <PlaceholderPage title="订单管理" />;
|
||||
};
|
||||
|
||||
export default Orders;
|
||||
8
nkebao/src/pages/plans/PlanDetail.tsx
Normal file
8
nkebao/src/pages/plans/PlanDetail.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const PlanDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="计划详情" />;
|
||||
};
|
||||
|
||||
export default PlanDetail;
|
||||
10
nkebao/src/pages/plans/Plans.tsx
Normal file
10
nkebao/src/pages/plans/Plans.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const Plans: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="计划管理" showAddButton addButtonText="新建计划" />
|
||||
);
|
||||
};
|
||||
|
||||
export default Plans;
|
||||
8
nkebao/src/pages/profile/Profile.tsx
Normal file
8
nkebao/src/pages/profile/Profile.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
return <PlaceholderPage title="个人中心" showBack={false} />;
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
10
nkebao/src/pages/scenarios/Scenarios.tsx
Normal file
10
nkebao/src/pages/scenarios/Scenarios.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const Scenarios: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="场景管理" showAddButton addButtonText="新建场景" />
|
||||
);
|
||||
};
|
||||
|
||||
export default Scenarios;
|
||||
26
nkebao/src/pages/scenarios/list/api.ts
Normal file
26
nkebao/src/pages/scenarios/list/api.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import request from '@/api/request';
|
||||
|
||||
// 获取场景列表
|
||||
export function getScenarios(params: any) {
|
||||
return request('/v1/plan/scenes', params, 'GET');
|
||||
}
|
||||
|
||||
// 获取场景详情
|
||||
export function getScenarioDetail(id: string) {
|
||||
return request(`/v1/scenarios/${id}`, {}, 'GET');
|
||||
}
|
||||
|
||||
// 创建场景
|
||||
export function createScenario(data: any) {
|
||||
return request('/v1/scenarios', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新场景
|
||||
export function updateScenario(id: string, data: any) {
|
||||
return request(`/v1/scenarios/${id}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除场景
|
||||
export function deleteScenario(id: string) {
|
||||
return request(`/v1/scenarios/${id}`, {}, 'DELETE');
|
||||
}
|
||||
322
nkebao/src/pages/scenarios/list/index.module.scss
Normal file
322
nkebao/src/pages/scenarios/list/index.module.scss
Normal file
@@ -0,0 +1,322 @@
|
||||
// 导航栏样式
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.new-plan-btn {
|
||||
border-radius: 20px;
|
||||
padding: 4px 12px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px var(--primary-shadow);
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 4px var(--primary-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面容器
|
||||
.scene-page {
|
||||
background: #f5f6fa;
|
||||
min-height: 100vh;
|
||||
padding: 0 0 60px 0;
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
.error-notice {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #fff2e8;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(255, 213, 145, 0.2);
|
||||
}
|
||||
|
||||
.error-notice-text {
|
||||
font-size: 12px;
|
||||
color: #d46b08;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #ff4d4f;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
min-width: 100px;
|
||||
border-radius: 20px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px var(--primary-shadow);
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.scene-header {
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 20px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 场景列表
|
||||
.scenarios-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// 场景卡片
|
||||
.scenario-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.scenarios-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 18px 10px 14px 10px;
|
||||
}
|
||||
|
||||
.card-img-wrap {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-img-bg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
text-align: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.card-count {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
.card-growth {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
border-radius: 20px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px var(--primary-shadow);
|
||||
padding: 6px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 480px) {
|
||||
|
||||
.scenario-card {
|
||||
padding: 14px 16px;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.scenario-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.scenario-image {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.scenario-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.scenario-growth {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.growth-icon {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.scenarios-grid {
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.card-inner {
|
||||
padding: 12px 4px 10px 4px;
|
||||
}
|
||||
.card-img-bg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.card-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: 11px;
|
||||
}
|
||||
.card-count {
|
||||
font-size: 12px;
|
||||
}
|
||||
.card-growth {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
180
nkebao/src/pages/scenarios/list/index.tsx
Normal file
180
nkebao/src/pages/scenarios/list/index.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NavBar, Button, Toast } from "antd-mobile";
|
||||
import { PlusOutlined, RiseOutlined } from "@ant-design/icons";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { getScenarios } from "./api";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface Scenario {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
description?: string;
|
||||
count: number;
|
||||
growth: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const scenarioDescriptions: Record<string, string> = {
|
||||
douyin: "通过抖音平台进行精准获客",
|
||||
xiaohongshu: "利用小红书平台进行内容营销获客",
|
||||
gongzhonghao: "通过微信公众号进行获客",
|
||||
haibao: "通过海报分享进行获客",
|
||||
phone: "通过电话营销进行获客",
|
||||
weixinqun: "通过微信群进行获客",
|
||||
payment: "通过付款码进行获客",
|
||||
api: "通过API接口进行获客",
|
||||
};
|
||||
|
||||
const Scene: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [scenarios, setScenarios] = useState<Scenario[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchScenarios = async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await getScenarios({ page: 1, limit: 20 });
|
||||
const transformedScenarios: Scenario[] = response.map((item: any) => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
image:
|
||||
item.image ||
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png",
|
||||
description:
|
||||
scenarioDescriptions[item.name?.toLowerCase()] ||
|
||||
"通过该平台进行获客",
|
||||
count: item.count,
|
||||
growth: item.growth,
|
||||
status: item.status,
|
||||
}));
|
||||
setScenarios(transformedScenarios);
|
||||
} catch (error) {
|
||||
setError("获取场景数据失败,请稍后重试");
|
||||
Toast.show({
|
||||
content: "获取场景数据失败,请稍后重试",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchScenarios();
|
||||
}, []);
|
||||
|
||||
const handleScenarioClick = (scenarioId: string, scenarioName: string) => {
|
||||
navigate(
|
||||
`/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`
|
||||
);
|
||||
};
|
||||
|
||||
const handleNewPlan = () => {
|
||||
navigate("/scenarios/new");
|
||||
};
|
||||
|
||||
if (error && scenarios.length === 0) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>场景获客</div>
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleNewPlan}
|
||||
className={style["new-plan-btn"]}
|
||||
style={{ marginLeft: "auto" }}
|
||||
>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["error"]}>
|
||||
<div className={style["error-text"]}>{error}</div>
|
||||
<Button color="primary" onClick={() => window.location.reload()}>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
loading={loading}
|
||||
header={
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={<div className={style["nav-title"]}>场景获客</div>}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleNewPlan}
|
||||
className={style["new-plan-btn"]}
|
||||
style={{ marginLeft: "auto" }}
|
||||
>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
}
|
||||
></NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["scene-page"]}>
|
||||
<div className={style["scenarios-grid"]}>
|
||||
{scenarios.map((scenario) => (
|
||||
<div
|
||||
key={scenario.id}
|
||||
className={style["scenario-card"]}
|
||||
onClick={() => handleScenarioClick(scenario.id, scenario.name)}
|
||||
>
|
||||
<div className={style["card-inner"]}>
|
||||
<div className={style["card-img-wrap"]}>
|
||||
<div className={style["card-img-bg"]}>
|
||||
<img
|
||||
src={scenario.image}
|
||||
alt={scenario.name}
|
||||
className={style["card-img"]}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src =
|
||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["card-title"]}>{scenario.name}</div>
|
||||
{scenario.description && (
|
||||
<div className={style["card-desc"]}>
|
||||
{scenario.description}
|
||||
</div>
|
||||
)}
|
||||
<div className={style["card-stats"]}>
|
||||
<span className={style["card-count"]}>
|
||||
今日: {scenario.count}
|
||||
</span>
|
||||
<span className={style["card-growth"]}>
|
||||
<RiseOutlined
|
||||
style={{ fontSize: 14, color: "#52c41a", marginRight: 2 }}
|
||||
/>
|
||||
{scenario.growth}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scene;
|
||||
32
nkebao/src/pages/scenarios/plan/list/api.ts
Normal file
32
nkebao/src/pages/scenarios/plan/list/api.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import request from "@/api/request";
|
||||
import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
|
||||
|
||||
// ==================== 计划相关接口 ====================
|
||||
// 获取计划列表
|
||||
export function getPlanList(params: {
|
||||
sceneId: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}): Promise<PlanListResponse> {
|
||||
return request(`/v1/plan/list`, params, "GET");
|
||||
}
|
||||
|
||||
// 获取计划详情
|
||||
export function getPlanDetail(planId: string): Promise<PlanDetail> {
|
||||
return request(`/v1/plan/detail`, { planId }, "GET");
|
||||
}
|
||||
|
||||
// 复制计划
|
||||
export function copyPlan(planId: string): Promise<ApiResponse<any>> {
|
||||
return request(`/v1/plan/copy`, { planId }, "GET");
|
||||
}
|
||||
|
||||
// 删除计划
|
||||
export function deletePlan(planId: string): Promise<ApiResponse<any>> {
|
||||
return request(`/v1/plan/delete`, { planId }, "DELETE");
|
||||
}
|
||||
|
||||
// 获取小程序二维码
|
||||
export function getWxMinAppCode(planId: string): Promise<ApiResponse<string>> {
|
||||
return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET");
|
||||
}
|
||||
59
nkebao/src/pages/scenarios/plan/list/data.ts
Normal file
59
nkebao/src/pages/scenarios/plan/list/data.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
enabled: boolean;
|
||||
total_customers?: number;
|
||||
today_customers?: number;
|
||||
lastUpdated?: string;
|
||||
stats?: {
|
||||
devices?: number;
|
||||
acquired?: number;
|
||||
added?: number;
|
||||
};
|
||||
reqConf?: {
|
||||
device?: string[];
|
||||
selectedDevices?: string[];
|
||||
};
|
||||
acquiredCount?: number;
|
||||
addedCount?: number;
|
||||
passRate?: number;
|
||||
}
|
||||
|
||||
export interface ApiSettings {
|
||||
apiKey: string;
|
||||
webhookUrl: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
// API响应相关类型
|
||||
export interface TextUrl {
|
||||
apiKey: string;
|
||||
originalString?: string;
|
||||
sign?: string;
|
||||
fullUrl: string;
|
||||
}
|
||||
|
||||
export interface PlanDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
scenario: number;
|
||||
enabled: boolean;
|
||||
status: number;
|
||||
apiKey: string;
|
||||
textUrl: TextUrl;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PlanListResponse {
|
||||
list: Task[];
|
||||
total: number;
|
||||
}
|
||||
401
nkebao/src/pages/scenarios/plan/list/index.module.scss
Normal file
401
nkebao/src/pages/scenarios/plan/list/index.module.scss
Normal file
@@ -0,0 +1,401 @@
|
||||
.scenario-list-page {
|
||||
padding:0 16px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.new-plan-btn {
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
.ant-input {
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.plan-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plan-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.plan-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
padding: 4px;
|
||||
min-width: auto;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.plan-footer {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.last-execution {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
svg {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.create-first-btn {
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
// 加载更多按钮样式
|
||||
.load-more-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
height: 44px;
|
||||
padding: 0 32px;
|
||||
border-radius: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 没有更多数据提示样式
|
||||
.no-more-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 40px;
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -50px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-menu-dialog {
|
||||
background: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-dialog {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
gap: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qr-error {
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
font-size: 14px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.qr-link-section {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.link-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link-input-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-input {
|
||||
flex: 1;
|
||||
|
||||
.ant-input {
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
&:focus {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
620
nkebao/src/pages/scenarios/plan/list/index.tsx
Normal file
620
nkebao/src/pages/scenarios/plan/list/index.tsx
Normal file
@@ -0,0 +1,620 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
NavBar,
|
||||
Button,
|
||||
Toast,
|
||||
SpinLoading,
|
||||
Dialog,
|
||||
Popup,
|
||||
Card,
|
||||
Tag,
|
||||
} from "antd-mobile";
|
||||
import { Input } from "antd";
|
||||
import {
|
||||
PlusOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
SettingOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
QrcodeOutlined,
|
||||
EditOutlined,
|
||||
MoreOutlined,
|
||||
ClockCircleOutlined,
|
||||
DownOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { LeftOutline } from "antd-mobile-icons";
|
||||
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import {
|
||||
getPlanList,
|
||||
getPlanDetail,
|
||||
copyPlan,
|
||||
deletePlan,
|
||||
getWxMinAppCode,
|
||||
} from "./api";
|
||||
import style from "./index.module.scss";
|
||||
import { Task, ApiSettings, PlanDetail } from "./data";
|
||||
import PlanApi from "./planApi";
|
||||
import { buildApiUrl } from "@/utils/apiUrl";
|
||||
|
||||
const ScenarioList: React.FC = () => {
|
||||
const { scenarioId, scenarioName } = useParams<{
|
||||
scenarioId: string;
|
||||
scenarioName: string;
|
||||
}>();
|
||||
const navigate = useNavigate();
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showApiDialog, setShowApiDialog] = useState(false);
|
||||
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
|
||||
apiKey: "",
|
||||
webhookUrl: "",
|
||||
taskId: "",
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [loadingTasks, setLoadingTasks] = useState(false);
|
||||
const [showQrDialog, setShowQrDialog] = useState(false);
|
||||
const [qrLoading, setQrLoading] = useState(false);
|
||||
const [qrImg, setQrImg] = useState<any>("");
|
||||
const [currentTaskId, setCurrentTaskId] = useState<string>("");
|
||||
const [showActionMenu, setShowActionMenu] = useState<string | null>(null);
|
||||
|
||||
// 分页相关状态
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 20;
|
||||
|
||||
// 获取渠道中文名称
|
||||
const getChannelName = (channel: string) => {
|
||||
const channelMap: Record<string, string> = {
|
||||
douyin: "抖音直播获客",
|
||||
kuaishou: "快手直播获客",
|
||||
xiaohongshu: "小红书种草获客",
|
||||
weibo: "微博话题获客",
|
||||
haibao: "海报扫码获客",
|
||||
phone: "电话号码获客",
|
||||
gongzhonghao: "公众号引流获客",
|
||||
weixinqun: "微信群裂变获客",
|
||||
payment: "付款码获客",
|
||||
api: "API接口获客",
|
||||
};
|
||||
return channelMap[channel] || `${channel}获客`;
|
||||
};
|
||||
|
||||
// 获取计划列表数据
|
||||
const fetchPlanList = async (page: number, isLoadMore: boolean = false) => {
|
||||
if (!scenarioId) return;
|
||||
|
||||
if (isLoadMore) {
|
||||
setLoadingMore(true);
|
||||
} else {
|
||||
setLoadingTasks(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getPlanList({
|
||||
sceneId: scenarioId,
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
});
|
||||
|
||||
if (response && response.list) {
|
||||
if (isLoadMore) {
|
||||
// 加载更多时,追加数据
|
||||
setTasks((prev) => [...prev, ...response.list]);
|
||||
} else {
|
||||
// 首次加载或刷新时,替换数据
|
||||
setTasks(response.list);
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
setTotal(response.total || 0);
|
||||
setHasMore(response.list.length === pageSize);
|
||||
setCurrentPage(page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取计划列表失败:", error);
|
||||
if (!isLoadMore) {
|
||||
setTasks([]);
|
||||
}
|
||||
Toast.show({
|
||||
content: "获取数据失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
if (isLoadMore) {
|
||||
setLoadingMore(false);
|
||||
} else {
|
||||
setLoadingTasks(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchScenarioData = async () => {
|
||||
if (!scenarioId) return;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await fetchPlanList(1, false);
|
||||
} catch (error) {
|
||||
console.error("获取场景数据失败:", error);
|
||||
setTasks([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchScenarioData();
|
||||
}, [scenarioId]);
|
||||
|
||||
// 加载下一页数据
|
||||
const handleLoadMore = async () => {
|
||||
if (loadingMore || !hasMore) return;
|
||||
await fetchPlanList(currentPage + 1, true);
|
||||
};
|
||||
|
||||
const handleCopyPlan = async (taskId: string) => {
|
||||
const taskToCopy = tasks.find((task) => task.id === taskId);
|
||||
if (!taskToCopy) return;
|
||||
|
||||
try {
|
||||
await copyPlan(taskId);
|
||||
Toast.show({
|
||||
content: `已成功复制"${taskToCopy.name}"`,
|
||||
position: "top",
|
||||
});
|
||||
// 刷新列表
|
||||
handleRefresh();
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "复制失败,请重试",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePlan = async (taskId: string) => {
|
||||
const taskToDelete = tasks.find((task) => task.id === taskId);
|
||||
if (!taskToDelete) return;
|
||||
|
||||
const result = await Dialog.confirm({
|
||||
content: `确定要删除"${taskToDelete.name}"吗?`,
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
});
|
||||
|
||||
if (result) {
|
||||
try {
|
||||
await deletePlan(taskId);
|
||||
Toast.show({
|
||||
content: "计划已删除",
|
||||
position: "top",
|
||||
});
|
||||
// 刷新列表
|
||||
handleRefresh();
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "删除失败,请重试",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenApiSettings = async (taskId: string) => {
|
||||
try {
|
||||
const response: PlanDetail = await getPlanDetail(taskId);
|
||||
if (response) {
|
||||
// 处理webhook URL,使用工具函数构建完整地址
|
||||
const webhookUrl = buildApiUrl(
|
||||
response.textUrl?.fullUrl || `webhook/${taskId}`
|
||||
);
|
||||
|
||||
setCurrentApiSettings({
|
||||
apiKey: response.apiKey || "demo-api-key-123456",
|
||||
webhookUrl: webhookUrl,
|
||||
taskId: taskId,
|
||||
});
|
||||
setShowApiDialog(true);
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "获取计划接口失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNewPlan = () => {
|
||||
navigate(`/scenarios/new/${scenarioId}`);
|
||||
};
|
||||
|
||||
const handleShowQrCode = async (taskId: string) => {
|
||||
setQrLoading(true);
|
||||
setShowQrDialog(true);
|
||||
setQrImg("");
|
||||
setCurrentTaskId(taskId); // 设置当前任务ID
|
||||
|
||||
try {
|
||||
const response = await getWxMinAppCode(taskId);
|
||||
setQrImg(response);
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "获取二维码失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setQrLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "success";
|
||||
case 0:
|
||||
return "default";
|
||||
case -1:
|
||||
return "danger";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "进行中";
|
||||
case 0:
|
||||
return "已暂停";
|
||||
case -1:
|
||||
return "已停止";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
// 重置分页状态
|
||||
setCurrentPage(1);
|
||||
setHasMore(true);
|
||||
await fetchPlanList(1, false);
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter((task) =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// 生成操作菜单
|
||||
const getActionMenu = (task: Task) => [
|
||||
{
|
||||
key: "edit",
|
||||
text: "编辑计划",
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => {
|
||||
setShowActionMenu(null);
|
||||
navigate(`/scenarios/edit/${task.id}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "copy",
|
||||
text: "复制计划",
|
||||
icon: <CopyOutlined />,
|
||||
onClick: () => {
|
||||
setShowActionMenu(null);
|
||||
handleCopyPlan(task.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
text: "计划接口",
|
||||
icon: <SettingOutlined />,
|
||||
onClick: () => {
|
||||
setShowActionMenu(null);
|
||||
handleOpenApiSettings(task.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "qrcode",
|
||||
text: "二维码",
|
||||
icon: <QrcodeOutlined />,
|
||||
onClick: () => {
|
||||
setShowActionMenu(null);
|
||||
handleShowQrCode(task.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
text: "删除计划",
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => {
|
||||
setShowActionMenu(null);
|
||||
handleDeletePlan(task.id);
|
||||
},
|
||||
danger: true,
|
||||
},
|
||||
];
|
||||
|
||||
const deviceCount = (task: Task) => {
|
||||
return Array.isArray(task.reqConf?.device)
|
||||
? task.reqConf!.device.length
|
||||
: Array.isArray(task.reqConf?.selectedDevices)
|
||||
? task.reqConf!.selectedDevices.length
|
||||
: 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div className={style["nav-title"]}>
|
||||
<span style={{ verticalAlign: "middle" }}>
|
||||
<LeftOutline onClick={() => navigate(-1)} fontSize={24} />
|
||||
</span>
|
||||
{scenarioName}
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleCreateNewPlan}
|
||||
className={style["new-plan-btn"]}
|
||||
>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{/* 搜索栏 */}
|
||||
<div className={style["search-bar"]}>
|
||||
<div className={style["search-input-wrapper"]}>
|
||||
<Input
|
||||
placeholder="搜索计划名称"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
loading={loadingTasks}
|
||||
className={style["refresh-btn"]}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
loading={loading}
|
||||
>
|
||||
<div className={style["scenario-list-page"]}>
|
||||
{/* 计划列表 */}
|
||||
<div className={style["plan-list"]}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className={style["empty-state"]}>
|
||||
<div className={style["empty-text"]}>
|
||||
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleCreateNewPlan}
|
||||
className={style["create-first-btn"]}
|
||||
>
|
||||
<PlusOutlined /> 创建第一个计划
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{filteredTasks.map((task) => (
|
||||
<Card key={task.id} className={style["plan-item"]}>
|
||||
{/* 头部:标题、状态和操作菜单 */}
|
||||
<div className={style["plan-header"]}>
|
||||
<div className={style["plan-name"]}>{task.name}</div>
|
||||
<div className={style["plan-header-right"]}>
|
||||
<Tag color={getStatusColor(task.status)}>
|
||||
{getStatusText(task.status)}
|
||||
</Tag>
|
||||
<Button
|
||||
size="mini"
|
||||
fill="none"
|
||||
className={style["more-btn"]}
|
||||
onClick={() => setShowActionMenu(task.id)}
|
||||
>
|
||||
<MoreOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数据网格 */}
|
||||
<div className={style["stats-grid"]}>
|
||||
<div className={style["stat-item"]}>
|
||||
<div className={style["stat-label"]}>设备数</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{deviceCount(task)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["stat-item"]}>
|
||||
<div className={style["stat-label"]}>已获客</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{task?.acquiredCount || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["stat-item"]}>
|
||||
<div className={style["stat-label"]}>已添加</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{task.addedCount || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["stat-item"]}>
|
||||
<div className={style["stat-label"]}>通过率</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{task.passRate}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部:上次执行时间 */}
|
||||
<div className={style["plan-footer"]}>
|
||||
<div className={style["last-execution"]}>
|
||||
<ClockCircleOutlined />
|
||||
<span>上次执行: {task.lastUpdated || "--"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* 加载更多按钮 */}
|
||||
{hasMore && (
|
||||
<div className={style["load-more-container"]}>
|
||||
<Button
|
||||
color="primary"
|
||||
fill="outline"
|
||||
size="large"
|
||||
onClick={handleLoadMore}
|
||||
loading={loadingMore}
|
||||
className={style["load-more-btn"]}
|
||||
>
|
||||
{loadingMore ? (
|
||||
<>
|
||||
<SpinLoading color="primary" />
|
||||
加载中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DownOutlined />
|
||||
加载更多
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 没有更多数据提示 */}
|
||||
{!hasMore && filteredTasks.length > 0 && (
|
||||
<div className={style["no-more-data"]}>
|
||||
<span>没有更多数据了</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 计划接口弹窗 */}
|
||||
<PlanApi
|
||||
visible={showApiDialog}
|
||||
onClose={() => setShowApiDialog(false)}
|
||||
apiKey={currentApiSettings.apiKey}
|
||||
webhookUrl={currentApiSettings.webhookUrl}
|
||||
taskId={currentApiSettings.taskId}
|
||||
/>
|
||||
|
||||
{/* 操作菜单弹窗 */}
|
||||
<Popup
|
||||
visible={!!showActionMenu}
|
||||
onMaskClick={() => setShowActionMenu(null)}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "auto", maxHeight: "60vh" }}
|
||||
>
|
||||
<div className={style["action-menu-dialog"]}>
|
||||
<div className={style["dialog-header"]}>
|
||||
<h3>操作菜单</h3>
|
||||
<Button size="small" onClick={() => setShowActionMenu(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["dialog-content"]}>
|
||||
{showActionMenu &&
|
||||
getActionMenu(tasks.find((t) => t.id === showActionMenu)!).map(
|
||||
(item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`${style["action-menu-item"]} ${item.danger ? style["danger"] : ""}`}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<span className={style["action-icon"]}>{item.icon}</span>
|
||||
<span className={style["action-text"]}>{item.text}</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 二维码弹窗 */}
|
||||
<Popup
|
||||
visible={showQrDialog}
|
||||
onMaskClick={() => setShowQrDialog(false)}
|
||||
position="bottom"
|
||||
>
|
||||
<div className={style["qr-dialog"]}>
|
||||
<div className={style["dialog-header"]}>
|
||||
<h3>小程序二维码</h3>
|
||||
<Button size="small" onClick={() => setShowQrDialog(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["dialog-content"]}>
|
||||
{qrLoading ? (
|
||||
<div className={style["qr-loading"]}>
|
||||
<SpinLoading color="primary" />
|
||||
<div>生成二维码中...</div>
|
||||
</div>
|
||||
) : qrImg ? (
|
||||
<>
|
||||
<img
|
||||
src={qrImg}
|
||||
alt="小程序二维码"
|
||||
className={style["qr-image"]}
|
||||
/>
|
||||
{/* 链接复制区域 */}
|
||||
<div className={style["qr-link-section"]}>
|
||||
<div className={style["link-label"]}>小程序链接</div>
|
||||
<div className={style["link-input-wrapper"]}>
|
||||
<Input
|
||||
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`}
|
||||
readOnly
|
||||
className={style["link-input"]}
|
||||
placeholder="小程序链接"
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`;
|
||||
navigator.clipboard.writeText(link);
|
||||
Toast.show({
|
||||
content: "链接已复制到剪贴板",
|
||||
position: "top",
|
||||
});
|
||||
}}
|
||||
className={style["copy-button"]}
|
||||
>
|
||||
<CopyOutlined />
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={style["qr-error"]}>二维码生成失败</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScenarioList;
|
||||
601
nkebao/src/pages/scenarios/plan/list/planApi.module.scss
Normal file
601
nkebao/src/pages/scenarios/plan/list/planApi.module.scss
Normal file
@@ -0,0 +1,601 @@
|
||||
// 移动端样式
|
||||
.plan-api-dialog {
|
||||
background: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 24px;
|
||||
color: #1890ff;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
padding: 12px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #1890ff;
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
|
||||
.security-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
svg {
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.complete-btn {
|
||||
height: 36px;
|
||||
padding: 0 24px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// 配置内容样式
|
||||
.config-content {
|
||||
.config-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.section-icon {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.api-input {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.security-tip {
|
||||
padding: 12px;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #d46b08;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.param-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.param-list {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
|
||||
div {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试内容样式
|
||||
.test-content {
|
||||
.test-section {
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-input {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.test-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文档内容样式
|
||||
.docs-content {
|
||||
.docs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.doc-card {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.doc-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #f0f8ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
font-size: 24px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 代码内容样式
|
||||
.code-content {
|
||||
.language-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
.lang-tab {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-block {
|
||||
position: relative;
|
||||
background: #f6f8fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e1e4e8;
|
||||
overflow: hidden;
|
||||
|
||||
.code {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #24292e;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PC端样式覆盖
|
||||
.plan-api-modal {
|
||||
.plan-api-dialog {
|
||||
border-radius: 12px;
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
.nav-tab {
|
||||
min-width: 100px;
|
||||
padding: 16px 12px;
|
||||
font-size: 15px;
|
||||
|
||||
svg {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.docs-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.plan-api-dialog {
|
||||
.header-content {
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
.nav-tab {
|
||||
font-size: 13px;
|
||||
padding: 10px 6px;
|
||||
|
||||
svg {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
.params-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
.docs-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.test-content {
|
||||
.test-buttons {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-content {
|
||||
.language-tabs {
|
||||
.lang-tab {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block {
|
||||
.code {
|
||||
font-size: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题支持
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.plan-api-dialog {
|
||||
background: #1f1f1f;
|
||||
color: #fff;
|
||||
|
||||
.dialog-header {
|
||||
background: #262626;
|
||||
border-bottom-color: #434343;
|
||||
|
||||
.header-content {
|
||||
h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
background: #262626;
|
||||
border-bottom-color: #434343;
|
||||
|
||||
.nav-tab {
|
||||
color: #a6a6a6;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #1890ff;
|
||||
border-bottom-color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
background: #262626;
|
||||
border-top-color: #434343;
|
||||
|
||||
.security-note {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.config-content {
|
||||
.section-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.security-tip {
|
||||
background: #2a1f00;
|
||||
border-color: #d48806;
|
||||
color: #ffc53d;
|
||||
}
|
||||
|
||||
.param-section {
|
||||
background: #262626;
|
||||
border-color: #434343;
|
||||
|
||||
h4 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.param-list {
|
||||
color: #a6a6a6;
|
||||
|
||||
code {
|
||||
background: #434343;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.test-content {
|
||||
h3 {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
.doc-card {
|
||||
background: #262626;
|
||||
border-color: #434343;
|
||||
|
||||
h4 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-content {
|
||||
.code-block {
|
||||
background: #0d1117;
|
||||
border-color: #30363d;
|
||||
|
||||
.code {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
437
nkebao/src/pages/scenarios/plan/list/planApi.tsx
Normal file
437
nkebao/src/pages/scenarios/plan/list/planApi.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { Popup, Button, Toast, SpinLoading } from "antd-mobile";
|
||||
import { Modal, Input, Tabs, Card, Tag, Space } from "antd";
|
||||
import {
|
||||
CopyOutlined,
|
||||
CodeOutlined,
|
||||
BookOutlined,
|
||||
ThunderboltOutlined,
|
||||
SettingOutlined,
|
||||
LinkOutlined,
|
||||
SafetyOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import style from "./planApi.module.scss";
|
||||
import { buildApiUrl } from "@/utils/apiUrl";
|
||||
|
||||
/**
|
||||
* 计划接口配置弹窗组件
|
||||
*
|
||||
* 使用示例:
|
||||
* ```tsx
|
||||
* const [showApiDialog, setShowApiDialog] = useState(false);
|
||||
* const [apiSettings, setApiSettings] = useState({
|
||||
* apiKey: "your-api-key",
|
||||
* webhookUrl: "https://api.example.com/webhook",
|
||||
* taskId: "task-123"
|
||||
* });
|
||||
*
|
||||
* <PlanApi
|
||||
* visible={showApiDialog}
|
||||
* onClose={() => setShowApiDialog(false)}
|
||||
* apiKey={apiSettings.apiKey}
|
||||
* webhookUrl={apiSettings.webhookUrl}
|
||||
* taskId={apiSettings.taskId}
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* 特性:
|
||||
* - 移动端使用 Popup,PC端使用 Modal
|
||||
* - 支持四个标签页:接口配置、快速测试、开发文档、代码示例
|
||||
* - 支持多种编程语言的代码示例
|
||||
* - 响应式设计,自适应不同屏幕尺寸
|
||||
* - 支持暗色主题
|
||||
* - 自动拼接API地址前缀
|
||||
*/
|
||||
|
||||
interface PlanApiProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
apiKey: string;
|
||||
webhookUrl: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
interface ApiSettings {
|
||||
apiKey: string;
|
||||
webhookUrl: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
const PlanApi: React.FC<PlanApiProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
apiKey,
|
||||
webhookUrl,
|
||||
taskId,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState("config");
|
||||
const [activeLanguage, setActiveLanguage] = useState("javascript");
|
||||
|
||||
// 处理webhook URL,确保包含完整的API地址
|
||||
const fullWebhookUrl = useMemo(() => {
|
||||
return buildApiUrl(webhookUrl);
|
||||
}, [webhookUrl]);
|
||||
|
||||
// 生成测试URL
|
||||
const testUrl = useMemo(() => {
|
||||
if (!fullWebhookUrl) return "";
|
||||
return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`;
|
||||
}, [fullWebhookUrl]);
|
||||
|
||||
// 检测是否为移动端
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
const handleCopy = (text: string, type: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
Toast.show({
|
||||
content: `${type}已复制到剪贴板`,
|
||||
position: "top",
|
||||
});
|
||||
};
|
||||
|
||||
const handleTestInBrowser = () => {
|
||||
window.open(testUrl, "_blank");
|
||||
};
|
||||
|
||||
const renderConfigTab = () => (
|
||||
<div className={style["config-content"]}>
|
||||
{/* API密钥配置 */}
|
||||
<div className={style["config-section"]}>
|
||||
<div className={style["section-header"]}>
|
||||
<div className={style["section-title"]}>
|
||||
<CheckCircleOutlined className={style["section-icon"]} />
|
||||
API密钥
|
||||
</div>
|
||||
<Tag color="green">安全认证</Tag>
|
||||
</div>
|
||||
<div className={style["input-group"]}>
|
||||
<Input value={apiKey} disabled className={style["api-input"]} />
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleCopy(apiKey, "API密钥")}
|
||||
className={style["copy-btn"]}
|
||||
>
|
||||
<CopyOutlined />
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["security-tip"]}>
|
||||
<strong>安全提示:</strong>
|
||||
请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 接口地址配置 */}
|
||||
<div className={style["config-section"]}>
|
||||
<div className={style["section-header"]}>
|
||||
<div className={style["section-title"]}>
|
||||
<LinkOutlined className={style["section-icon"]} />
|
||||
接口地址
|
||||
</div>
|
||||
<Tag color="blue">POST请求</Tag>
|
||||
</div>
|
||||
<div className={style["input-group"]}>
|
||||
<Input
|
||||
value={fullWebhookUrl}
|
||||
disabled
|
||||
className={style["api-input"]}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleCopy(fullWebhookUrl, "接口地址")}
|
||||
className={style["copy-btn"]}
|
||||
>
|
||||
<CopyOutlined />
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 参数说明 */}
|
||||
<div className={style["params-grid"]}>
|
||||
<div className={style["param-section"]}>
|
||||
<h4>必要参数</h4>
|
||||
<div className={style["param-list"]}>
|
||||
<div>
|
||||
<code>name</code> - 客户姓名
|
||||
</div>
|
||||
<div>
|
||||
<code>phone</code> - 手机号码
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["param-section"]}>
|
||||
<h4>可选参数</h4>
|
||||
<div className={style["param-list"]}>
|
||||
<div>
|
||||
<code>source</code> - 来源标识
|
||||
</div>
|
||||
<div>
|
||||
<code>remark</code> - 备注信息
|
||||
</div>
|
||||
<div>
|
||||
<code>tags</code> - 客户标签
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderQuickTestTab = () => (
|
||||
<div className={style["test-content"]}>
|
||||
<div className={style["test-section"]}>
|
||||
<h3>快速测试URL</h3>
|
||||
<div className={style["input-group"]}>
|
||||
<Input value={testUrl} disabled className={style["test-input"]} />
|
||||
</div>
|
||||
<div className={style["test-buttons"]}>
|
||||
<Button
|
||||
onClick={() => handleCopy(testUrl, "测试URL")}
|
||||
className={style["test-btn"]}
|
||||
>
|
||||
<CopyOutlined />
|
||||
复制测试URL
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleTestInBrowser}
|
||||
className={style["test-btn"]}
|
||||
>
|
||||
在浏览器中测试
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderDocsTab = () => (
|
||||
<div className={style["docs-content"]}>
|
||||
<div className={style["docs-grid"]}>
|
||||
<Card className={style["doc-card"]}>
|
||||
<div className={style["doc-icon"]}>
|
||||
<BookOutlined />
|
||||
</div>
|
||||
<h4>完整API文档</h4>
|
||||
<p>详细的接口说明和参数文档</p>
|
||||
</Card>
|
||||
<Card className={style["doc-card"]}>
|
||||
<div className={style["doc-icon"]}>
|
||||
<LinkOutlined />
|
||||
</div>
|
||||
<h4>集成指南</h4>
|
||||
<p>第三方平台集成教程</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderCodeTab = () => {
|
||||
const codeExamples = {
|
||||
javascript: `fetch('${fullWebhookUrl}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${apiKey}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: '张三',
|
||||
phone: '13800138000',
|
||||
source: '官网表单',
|
||||
})
|
||||
})`,
|
||||
python: `import requests
|
||||
|
||||
url = '${fullWebhookUrl}'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${apiKey}'
|
||||
}
|
||||
data = {
|
||||
'name': '张三',
|
||||
'phone': '13800138000',
|
||||
'source': '官网表单'
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data, headers=headers)`,
|
||||
php: `<?php
|
||||
$url = '${fullWebhookUrl}';
|
||||
$data = array(
|
||||
'name' => '张三',
|
||||
'phone' => '13800138000',
|
||||
'source' => '官网表单'
|
||||
);
|
||||
|
||||
$options = array(
|
||||
'http' => array(
|
||||
'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n",
|
||||
'method' => 'POST',
|
||||
'content' => json_encode($data)
|
||||
)
|
||||
);
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$result = file_get_contents($url, false, $context);`,
|
||||
java: `import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.URI;
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}";
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("${fullWebhookUrl}"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer ${apiKey}")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style["code-content"]}>
|
||||
<div className={style["language-tabs"]}>
|
||||
{Object.keys(codeExamples).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
className={`${style["lang-tab"]} ${
|
||||
activeLanguage === lang ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveLanguage(lang)}
|
||||
>
|
||||
{lang.charAt(0).toUpperCase() + lang.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={style["code-block"]}>
|
||||
<pre className={style["code"]}>
|
||||
<code>
|
||||
{codeExamples[activeLanguage as keyof typeof codeExamples]}
|
||||
</code>
|
||||
</pre>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
handleCopy(
|
||||
codeExamples[activeLanguage as keyof typeof codeExamples],
|
||||
"代码"
|
||||
)
|
||||
}
|
||||
className={style["copy-code-btn"]}
|
||||
>
|
||||
<CopyOutlined />
|
||||
复制代码
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<div className={style["plan-api-dialog"]}>
|
||||
{/* 头部 */}
|
||||
<div className={style["dialog-header"]}>
|
||||
<div className={style["header-left"]}>
|
||||
<CodeOutlined className={style["header-icon"]} />
|
||||
<div className={style["header-content"]}>
|
||||
<h3>计划接口配置</h3>
|
||||
<p>
|
||||
通过API接口直接导入客资到该获客计划,支持多种编程语言和第三方平台集成
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="small" onClick={onClose} className={style["close-btn"]}>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 导航标签 */}
|
||||
<div className={style["nav-tabs"]}>
|
||||
<button
|
||||
className={`${style["nav-tab"]} ${activeTab === "config" ? style["active"] : ""}`}
|
||||
onClick={() => setActiveTab("config")}
|
||||
>
|
||||
<SettingOutlined />
|
||||
接口配置
|
||||
</button>
|
||||
<button
|
||||
className={`${style["nav-tab"]} ${activeTab === "test" ? style["active"] : ""}`}
|
||||
onClick={() => setActiveTab("test")}
|
||||
>
|
||||
<ThunderboltOutlined />
|
||||
快速测试
|
||||
</button>
|
||||
<button
|
||||
className={`${style["nav-tab"]} ${activeTab === "docs" ? style["active"] : ""}`}
|
||||
onClick={() => setActiveTab("docs")}
|
||||
>
|
||||
<BookOutlined />
|
||||
开发文档
|
||||
</button>
|
||||
<button
|
||||
className={`${style["nav-tab"]} ${activeTab === "code" ? style["active"] : ""}`}
|
||||
onClick={() => setActiveTab("code")}
|
||||
>
|
||||
<CodeOutlined />
|
||||
代码示例
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className={style["dialog-content"]}>
|
||||
{activeTab === "config" && renderConfigTab()}
|
||||
{activeTab === "test" && renderQuickTestTab()}
|
||||
{activeTab === "docs" && renderDocsTab()}
|
||||
{activeTab === "code" && renderCodeTab()}
|
||||
</div>
|
||||
|
||||
{/* 底部 */}
|
||||
<div className={style["dialog-footer"]}>
|
||||
<div className={style["security-note"]}>
|
||||
<SafetyOutlined />
|
||||
所有数据传输均采用HTTPS加密
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClose}
|
||||
className={style["complete-btn"]}
|
||||
>
|
||||
完成配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 移动端使用Popup
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
onMaskClick={onClose}
|
||||
position="bottom"
|
||||
bodyStyle={{ height: "90vh" }}
|
||||
>
|
||||
{renderContent()}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
// PC端使用Modal
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width={800}
|
||||
centered
|
||||
className={style["plan-api-modal"]}
|
||||
>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanApi;
|
||||
293
nkebao/src/pages/scenarios/plan/new/index.tsx
Normal file
293
nkebao/src/pages/scenarios/plan/new/index.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { NavBar, Button, Toast, SpinLoading, Steps, Popup } from "antd-mobile";
|
||||
import { LeftOutline } from "antd-mobile-icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import BasicSettings from "./steps/BasicSettings";
|
||||
import FriendRequestSettings from "./steps/FriendRequestSettings";
|
||||
import MessageSettings from "./steps/MessageSettings";
|
||||
import {
|
||||
getScenarioTypes,
|
||||
createPlan,
|
||||
updatePlan,
|
||||
getPlanDetail,
|
||||
} from "./page.api";
|
||||
import style from "./page.module.scss";
|
||||
|
||||
// 步骤定义
|
||||
const steps = [
|
||||
{ id: 1, title: "步骤一", subtitle: "基础设置" },
|
||||
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
|
||||
{ id: 3, title: "步骤三", subtitle: "消息设置" },
|
||||
];
|
||||
|
||||
// 类型定义
|
||||
interface FormData {
|
||||
name: string;
|
||||
scenario: number;
|
||||
posters: any[];
|
||||
device: string[];
|
||||
remarkType: string;
|
||||
greeting: string;
|
||||
addInterval: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
enabled: boolean;
|
||||
sceneId: string | number;
|
||||
remarkFormat: string;
|
||||
addFriendInterval: number;
|
||||
}
|
||||
|
||||
const NewPlan: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
scenario: 1,
|
||||
posters: [],
|
||||
device: [],
|
||||
remarkType: "phone",
|
||||
greeting: "你好,请通过",
|
||||
addInterval: 1,
|
||||
startTime: "09:00",
|
||||
endTime: "18:00",
|
||||
enabled: true,
|
||||
sceneId: "",
|
||||
remarkFormat: "",
|
||||
addFriendInterval: 1,
|
||||
});
|
||||
const [sceneList, setSceneList] = useState<any[]>([]);
|
||||
const [sceneLoading, setSceneLoading] = useState(true);
|
||||
const { scenarioId, planId } = useParams<{
|
||||
scenarioId: string;
|
||||
planId: string;
|
||||
}>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setSceneLoading(true);
|
||||
try {
|
||||
// 获取场景类型
|
||||
const res = await getScenarioTypes();
|
||||
if (res?.data) {
|
||||
setSceneList(res.data);
|
||||
}
|
||||
|
||||
if (planId) {
|
||||
setIsEdit(true);
|
||||
// 获取计划详情
|
||||
const detailRes = await getPlanDetail(planId);
|
||||
if (detailRes.code === 200 && detailRes.data) {
|
||||
const detail = detailRes.data;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
name: detail.name ?? "",
|
||||
scenario: Number(detail.scenario) || 1,
|
||||
posters: detail.posters ?? [],
|
||||
device: detail.device ?? [],
|
||||
remarkType: detail.remarkType ?? "phone",
|
||||
greeting: detail.greeting ?? "",
|
||||
addInterval: detail.addInterval ?? 1,
|
||||
startTime: detail.startTime ?? "09:00",
|
||||
endTime: detail.endTime ?? "18:00",
|
||||
enabled: detail.enabled ?? true,
|
||||
sceneId: Number(detail.scenario) || 1,
|
||||
remarkFormat: detail.remarkFormat ?? "",
|
||||
addFriendInterval: detail.addFriendInterval ?? 1,
|
||||
}));
|
||||
}
|
||||
} else if (scenarioId) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
scenario: Number(scenarioId) || 1,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "加载数据失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setSceneLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新表单数据
|
||||
const onChange = (data: any) => {
|
||||
setFormData((prev) => ({ ...prev, ...data }));
|
||||
};
|
||||
|
||||
// 处理保存
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim()) {
|
||||
Toast.show({
|
||||
content: "请输入计划名称",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
let result;
|
||||
if (isEdit && planId) {
|
||||
// 编辑
|
||||
const editData = {
|
||||
...formData,
|
||||
id: Number(planId),
|
||||
planId: Number(planId),
|
||||
};
|
||||
result = await updatePlan(planId, editData);
|
||||
} else {
|
||||
// 新建
|
||||
result = await createPlan(formData);
|
||||
}
|
||||
|
||||
if (result.code === 200) {
|
||||
Toast.show({
|
||||
content: isEdit ? "计划已更新" : "获客计划已创建",
|
||||
position: "top",
|
||||
});
|
||||
const sceneItem = sceneList.find((v) => formData.scenario === v.id);
|
||||
navigate(
|
||||
`/scenarios/list/${formData.sceneId}/${sceneItem?.name || ""}`
|
||||
);
|
||||
} else {
|
||||
Toast.show({
|
||||
content: result.msg || "操作失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: isEdit ? "更新计划失败,请重试" : "创建计划失败,请重试",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 下一步
|
||||
const handleNext = () => {
|
||||
if (currentStep === steps.length) {
|
||||
handleSave();
|
||||
} else {
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 上一步
|
||||
const handlePrev = () => {
|
||||
setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||
};
|
||||
|
||||
// 渲染当前步骤内容
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<BasicSettings
|
||||
formData={formData}
|
||||
onChange={onChange}
|
||||
onNext={handleNext}
|
||||
sceneList={sceneList}
|
||||
sceneLoading={sceneLoading}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<FriendRequestSettings
|
||||
formData={formData}
|
||||
onChange={onChange}
|
||||
onNext={handleNext}
|
||||
onPrev={handlePrev}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<MessageSettings
|
||||
formData={formData}
|
||||
onChange={onChange}
|
||||
onNext={handleNext}
|
||||
onPrev={handlePrev}
|
||||
saving={saving}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (sceneLoading) {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>
|
||||
{isEdit ? "编辑计划" : "新建计划"}
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
<div className={style["loading-text"]}>加载数据中...</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div className={style["nav-title"]}>
|
||||
{isEdit ? "编辑计划" : "新建计划"}
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => navigate(-1)}
|
||||
className={style["back-btn"]}
|
||||
>
|
||||
<LeftOutline />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={style["new-plan-page"]}>
|
||||
{/* 步骤指示器 */}
|
||||
<div className={style["steps-container"]}>
|
||||
<Steps current={currentStep - 1}>
|
||||
{steps.map((step) => (
|
||||
<Steps.Step
|
||||
key={step.id}
|
||||
title={step.title}
|
||||
description={step.subtitle}
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
<div className={style["step-content"]}>{renderStepContent()}</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewPlan;
|
||||
20
nkebao/src/pages/scenarios/plan/new/page.api.ts
Normal file
20
nkebao/src/pages/scenarios/plan/new/page.api.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import request from "@/api/request";
|
||||
// 获取场景类型列表
|
||||
export function getScenarioTypes() {
|
||||
return request("/api/scenarios/types", undefined, "GET");
|
||||
}
|
||||
|
||||
// 创建计划
|
||||
export function createPlan(data: any) {
|
||||
return request("/api/scenarios/plans", data, "POST");
|
||||
}
|
||||
|
||||
// 更新计划
|
||||
export function updatePlan(planId: string, data: any) {
|
||||
return request(`/api/scenarios/plans/${planId}`, data, "PUT");
|
||||
}
|
||||
|
||||
// 获取计划详情
|
||||
export function getPlanDetail(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}`, undefined, "GET");
|
||||
}
|
||||
43
nkebao/src/pages/scenarios/plan/new/page.module.scss
Normal file
43
nkebao/src/pages/scenarios/plan/new/page.module.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.new-plan-page {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.steps-container {
|
||||
background: white;
|
||||
padding: 20px 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.basic-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-selector {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.time-input {
|
||||
width: 120px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40vh;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
209
nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx
Normal file
209
nkebao/src/pages/scenarios/plan/new/steps/BasicSettings.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Selector,
|
||||
Button,
|
||||
SpinLoading,
|
||||
Toast,
|
||||
Card,
|
||||
Space,
|
||||
} from "antd-mobile";
|
||||
// import { getDevices, getPosters } from "./step.api";
|
||||
import style from "./BasicSettings.module.scss";
|
||||
|
||||
interface BasicSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
sceneList: any[];
|
||||
sceneLoading: boolean;
|
||||
}
|
||||
|
||||
const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
sceneList,
|
||||
sceneLoading,
|
||||
}) => {
|
||||
const [devices, setDevices] = useState<any[]>([]);
|
||||
const [posters, setPosters] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// // 获取设备列表
|
||||
// const devicesRes = await getDevices();
|
||||
// if (devicesRes?.data) {
|
||||
// setDevices(devicesRes.data);
|
||||
// }
|
||||
// // 获取海报列表
|
||||
// const postersRes = await getPosters();
|
||||
// if (postersRes?.data) {
|
||||
// setPosters(postersRes.data);
|
||||
// }
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
content: "加载数据失败",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!formData.name.trim()) {
|
||||
Toast.show({
|
||||
content: "请输入计划名称",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.scenario) {
|
||||
Toast.show({
|
||||
content: "请选择场景类型",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.device.length === 0) {
|
||||
Toast.show({
|
||||
content: "请选择设备",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
if (loading || sceneLoading) {
|
||||
return (
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
<div className={style["loading-text"]}>加载数据中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style["basic-settings"]}>
|
||||
<Card className={style["form-card"]}>
|
||||
<Form layout="vertical">
|
||||
{/* 计划名称 */}
|
||||
<Form.Item label="计划名称" required className={style["form-item"]}>
|
||||
<Input
|
||||
placeholder="请输入计划名称"
|
||||
value={formData.name}
|
||||
onChange={(value) => onChange({ name: value })}
|
||||
clearable
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 场景类型 */}
|
||||
<Form.Item label="场景类型" required className={style["form-item"]}>
|
||||
<Selector
|
||||
options={sceneList.map((scene) => ({
|
||||
label: scene.name,
|
||||
value: scene.id,
|
||||
}))}
|
||||
value={[formData.scenario]}
|
||||
onChange={(value) => {
|
||||
const selectedScene = sceneList.find(
|
||||
(scene) => scene.id === value[0]
|
||||
);
|
||||
onChange({
|
||||
scenario: value[0],
|
||||
sceneId: value[0],
|
||||
name: selectedScene?.name || "",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 选择设备 */}
|
||||
<Form.Item label="选择设备" required className={style["form-item"]}>
|
||||
<Selector
|
||||
options={devices.map((device) => ({
|
||||
label: device.name,
|
||||
value: device.id,
|
||||
}))}
|
||||
value={formData.device}
|
||||
onChange={(value) => onChange({ device: value })}
|
||||
multiple
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 选择海报 */}
|
||||
<Form.Item label="选择海报" className={style["form-item"]}>
|
||||
<Selector
|
||||
options={posters.map((poster) => ({
|
||||
label: poster.name,
|
||||
value: poster.id,
|
||||
}))}
|
||||
value={formData.posters}
|
||||
onChange={(value) => onChange({ posters: value })}
|
||||
multiple
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 工作时间 */}
|
||||
<Form.Item label="工作时间" className={style["form-item"]}>
|
||||
<Space>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.startTime}
|
||||
onChange={(value) => onChange({ startTime: value })}
|
||||
className={style["time-input"]}
|
||||
/>
|
||||
<span>至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(value) => onChange({ endTime: value })}
|
||||
className={style["time-input"]}
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
{/* 添加间隔 */}
|
||||
<Form.Item label="添加间隔(分钟)" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入添加间隔"
|
||||
value={formData.addInterval.toString()}
|
||||
onChange={(value) =>
|
||||
onChange({ addInterval: Number(value) || 1 })
|
||||
}
|
||||
min={1}
|
||||
max={60}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["actions"]}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleNext}
|
||||
className={style["next-btn"]}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicSettings;
|
||||
@@ -0,0 +1,56 @@
|
||||
.friend-request-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-selector {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-text-area {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.prev-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Selector,
|
||||
Button,
|
||||
Card,
|
||||
Space,
|
||||
TextArea,
|
||||
} from "antd-mobile";
|
||||
import style from "./FriendRequestSettings.module.scss";
|
||||
|
||||
interface FriendRequestSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
}
|
||||
|
||||
const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
onPrev,
|
||||
}) => {
|
||||
const remarkTypeOptions = [
|
||||
{ label: "手机号", value: "phone" },
|
||||
{ label: "微信号", value: "wechat" },
|
||||
{ label: "QQ号", value: "qq" },
|
||||
{ label: "自定义", value: "custom" },
|
||||
];
|
||||
|
||||
const handleNext = () => {
|
||||
if (!formData.greeting.trim()) {
|
||||
// 可以添加验证逻辑
|
||||
}
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style["friend-request-settings"]}>
|
||||
<Card className={style["form-card"]}>
|
||||
<Form layout="vertical">
|
||||
{/* 备注类型 */}
|
||||
<Form.Item label="备注类型" required className={style["form-item"]}>
|
||||
<Selector
|
||||
options={remarkTypeOptions}
|
||||
value={[formData.remarkType]}
|
||||
onChange={(value) => onChange({ remarkType: value[0] })}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 备注格式 */}
|
||||
{formData.remarkType === "custom" && (
|
||||
<Form.Item label="备注格式" required className={style["form-item"]}>
|
||||
<Input
|
||||
placeholder="请输入备注格式,如:{name}-{phone}"
|
||||
value={formData.remarkFormat}
|
||||
onChange={(value) => onChange({ remarkFormat: value })}
|
||||
clearable
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 打招呼消息 */}
|
||||
<Form.Item label="打招呼消息" required className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入打招呼消息"
|
||||
value={formData.greeting}
|
||||
onChange={(value) => onChange({ greeting: value })}
|
||||
rows={4}
|
||||
maxLength={200}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 好友申请间隔 */}
|
||||
<Form.Item label="好友申请间隔(分钟)" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入好友申请间隔"
|
||||
value={formData.addFriendInterval.toString()}
|
||||
onChange={(value) =>
|
||||
onChange({ addFriendInterval: Number(value) || 1 })
|
||||
}
|
||||
min={1}
|
||||
max={60}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["actions"]}>
|
||||
<Space style={{ width: "100%" }}>
|
||||
<Button size="large" onClick={onPrev} className={style["prev-btn"]}>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleNext}
|
||||
className={style["next-btn"]}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FriendRequestSettings;
|
||||
@@ -0,0 +1,64 @@
|
||||
.message-settings {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.adm-text-area {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.prev-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
140
nkebao/src/pages/scenarios/plan/new/steps/MessageSettings.tsx
Normal file
140
nkebao/src/pages/scenarios/plan/new/steps/MessageSettings.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Selector,
|
||||
Button,
|
||||
Card,
|
||||
Space,
|
||||
TextArea,
|
||||
Switch,
|
||||
} from "antd-mobile";
|
||||
import style from "./MessageSettings.module.scss";
|
||||
|
||||
interface MessageSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
saving: boolean;
|
||||
}
|
||||
|
||||
const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
formData,
|
||||
onChange,
|
||||
onNext,
|
||||
onPrev,
|
||||
saving,
|
||||
}) => {
|
||||
const handleSave = () => {
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style["message-settings"]}>
|
||||
<Card className={style["form-card"]}>
|
||||
<Form layout="vertical">
|
||||
{/* 启用状态 */}
|
||||
<Form.Item label="启用状态" className={style["form-item"]}>
|
||||
<div className={style["switch-item"]}>
|
||||
<span>启用计划</span>
|
||||
<Switch
|
||||
checked={formData.enabled}
|
||||
onChange={(checked) => onChange({ enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* 自动回复消息 */}
|
||||
<Form.Item label="自动回复消息" className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入自动回复消息"
|
||||
value={formData.autoReply || ""}
|
||||
onChange={(value) => onChange({ autoReply: value })}
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 关键词回复 */}
|
||||
<Form.Item label="关键词回复" className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入关键词回复规则,格式:关键词=回复内容"
|
||||
value={formData.keywordReply || ""}
|
||||
onChange={(value) => onChange({ keywordReply: value })}
|
||||
rows={4}
|
||||
maxLength={1000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 群发消息 */}
|
||||
<Form.Item label="群发消息" className={style["form-item"]}>
|
||||
<TextArea
|
||||
placeholder="请输入群发消息内容"
|
||||
value={formData.groupMessage || ""}
|
||||
onChange={(value) => onChange({ groupMessage: value })}
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 消息发送间隔 */}
|
||||
<Form.Item label="消息发送间隔(秒)" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入消息发送间隔"
|
||||
value={formData.messageInterval?.toString() || "5"}
|
||||
onChange={(value) =>
|
||||
onChange({ messageInterval: Number(value) || 5 })
|
||||
}
|
||||
min={1}
|
||||
max={300}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 每日发送限制 */}
|
||||
<Form.Item label="每日发送限制" className={style["form-item"]}>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入每日发送限制数量"
|
||||
value={formData.dailyLimit?.toString() || "100"}
|
||||
onChange={(value) =>
|
||||
onChange({ dailyLimit: Number(value) || 100 })
|
||||
}
|
||||
min={1}
|
||||
max={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["actions"]}>
|
||||
<Space style={{ width: "100%" }}>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={onPrev}
|
||||
className={style["prev-btn"]}
|
||||
disabled={saving}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
onClick={handleSave}
|
||||
className={style["save-btn"]}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? "保存中..." : "保存计划"}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageSettings;
|
||||
363
nkebao/src/pages/scenarios/plan/new/steps/step.api.ts
Normal file
363
nkebao/src/pages/scenarios/plan/new/steps/step.api.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import request from '@/api/request';
|
||||
|
||||
// ==================== 场景相关接口 ====================
|
||||
|
||||
// 获取场景列表
|
||||
export function getScenarios(params: any) {
|
||||
return request('/v1/plan/scenes', params, 'GET');
|
||||
}
|
||||
|
||||
// 获取场景详情
|
||||
export function getScenarioDetail(id: string) {
|
||||
return request(`/v1/scenarios/${id}`, {}, 'GET');
|
||||
}
|
||||
|
||||
// 创建场景
|
||||
export function createScenario(data: any) {
|
||||
return request('/v1/scenarios', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新场景
|
||||
export function updateScenario(id: string, data: any) {
|
||||
return request(`/v1/scenarios/${id}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除场景
|
||||
export function deleteScenario(id: string) {
|
||||
return request(`/v1/scenarios/${id}`, {}, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 计划相关接口 ====================
|
||||
|
||||
// 获取计划列表
|
||||
export function getPlanList(scenarioId: string, page: number = 1, limit: number = 20) {
|
||||
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, 'GET');
|
||||
}
|
||||
// 复制计划
|
||||
export function copyPlan(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}/copy`, undefined, 'POST');
|
||||
}
|
||||
|
||||
// 删除计划
|
||||
export function deletePlan(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 获取小程序二维码
|
||||
export function getWxMinAppCode(planId: string) {
|
||||
return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, 'GET');
|
||||
}
|
||||
|
||||
|
||||
// ==================== 设备相关接口 ====================
|
||||
|
||||
// 获取设备列表
|
||||
export function getDevices() {
|
||||
return request('/api/devices', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 获取设备详情
|
||||
export function getDeviceDetail(deviceId: string) {
|
||||
return request(`/api/devices/${deviceId}`, undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建设备
|
||||
export function createDevice(data: any) {
|
||||
return request('/api/devices', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新设备
|
||||
export function updateDevice(deviceId: string, data: any) {
|
||||
return request(`/api/devices/${deviceId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
export function deleteDevice(deviceId: string) {
|
||||
return request(`/api/devices/${deviceId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 微信号相关接口 ====================
|
||||
|
||||
// 获取微信号列表
|
||||
export function getWechatAccounts() {
|
||||
return request('/api/wechat-accounts', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 获取微信号详情
|
||||
export function getWechatAccountDetail(accountId: string) {
|
||||
return request(`/api/wechat-accounts/${accountId}`, undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建微信号
|
||||
export function createWechatAccount(data: any) {
|
||||
return request('/api/wechat-accounts', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新微信号
|
||||
export function updateWechatAccount(accountId: string, data: any) {
|
||||
return request(`/api/wechat-accounts/${accountId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除微信号
|
||||
export function deleteWechatAccount(accountId: string) {
|
||||
return request(`/api/wechat-accounts/${accountId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 海报相关接口 ====================
|
||||
|
||||
// 获取海报列表
|
||||
export function getPosters() {
|
||||
return request('/api/posters', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 获取海报详情
|
||||
export function getPosterDetail(posterId: string) {
|
||||
return request(`/api/posters/${posterId}`, undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建海报
|
||||
export function createPoster(data: any) {
|
||||
return request('/api/posters', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新海报
|
||||
export function updatePoster(posterId: string, data: any) {
|
||||
return request(`/api/posters/${posterId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除海报
|
||||
export function deletePoster(posterId: string) {
|
||||
return request(`/api/posters/${posterId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 内容相关接口 ====================
|
||||
|
||||
// 获取内容列表
|
||||
export function getContents(params: any) {
|
||||
return request('/api/contents', params, 'GET');
|
||||
}
|
||||
|
||||
// 获取内容详情
|
||||
export function getContentDetail(contentId: string) {
|
||||
return request(`/api/contents/${contentId}`, undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建内容
|
||||
export function createContent(data: any) {
|
||||
return request('/api/contents', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
export function updateContent(contentId: string, data: any) {
|
||||
return request(`/api/contents/${contentId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除内容
|
||||
export function deleteContent(contentId: string) {
|
||||
return request(`/api/contents/${contentId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 流量池相关接口 ====================
|
||||
|
||||
// 获取流量池列表
|
||||
export function getTrafficPools() {
|
||||
return request('/api/traffic-pools', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 获取流量池详情
|
||||
export function getTrafficPoolDetail(poolId: string) {
|
||||
return request(`/api/traffic-pools/${poolId}`, undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建流量池
|
||||
export function createTrafficPool(data: any) {
|
||||
return request('/api/traffic-pools', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新流量池
|
||||
export function updateTrafficPool(poolId: string, data: any) {
|
||||
return request(`/api/traffic-pools/${poolId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除流量池
|
||||
export function deleteTrafficPool(poolId: string) {
|
||||
return request(`/api/traffic-pools/${poolId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 工作台相关接口 ====================
|
||||
|
||||
// 获取工作台统计数据
|
||||
export function getWorkspaceStats() {
|
||||
return request('/api/workspace/stats', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export function getAutoLikeTasks() {
|
||||
return request('/api/workspace/auto-like/tasks', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建自动点赞任务
|
||||
export function createAutoLikeTask(data: any) {
|
||||
return request('/api/workspace/auto-like/tasks', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新自动点赞任务
|
||||
export function updateAutoLikeTask(taskId: string, data: any) {
|
||||
return request(`/api/workspace/auto-like/tasks/${taskId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除自动点赞任务
|
||||
export function deleteAutoLikeTask(taskId: string) {
|
||||
return request(`/api/workspace/auto-like/tasks/${taskId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 群发相关接口 ====================
|
||||
|
||||
// 获取群发任务列表
|
||||
export function getGroupPushTasks() {
|
||||
return request('/api/workspace/group-push/tasks', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建群发任务
|
||||
export function createGroupPushTask(data: any) {
|
||||
return request('/api/workspace/group-push/tasks', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新群发任务
|
||||
export function updateGroupPushTask(taskId: string, data: any) {
|
||||
return request(`/api/workspace/group-push/tasks/${taskId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除群发任务
|
||||
export function deleteGroupPushTask(taskId: string) {
|
||||
return request(`/api/workspace/group-push/tasks/${taskId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 自动建群相关接口 ====================
|
||||
|
||||
// 获取自动建群任务列表
|
||||
export function getAutoGroupTasks() {
|
||||
return request('/api/workspace/auto-group/tasks', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建自动建群任务
|
||||
export function createAutoGroupTask(data: any) {
|
||||
return request('/api/workspace/auto-group/tasks', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新自动建群任务
|
||||
export function updateAutoGroupTask(taskId: string, data: any) {
|
||||
return request(`/api/workspace/auto-group/tasks/${taskId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除自动建群任务
|
||||
export function deleteAutoGroupTask(taskId: string) {
|
||||
return request(`/api/workspace/auto-group/tasks/${taskId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== AI助手相关接口 ====================
|
||||
|
||||
// 获取AI对话历史
|
||||
export function getAIChatHistory() {
|
||||
return request('/api/workspace/ai-assistant/chat-history', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 发送AI消息
|
||||
export function sendAIMessage(data: any) {
|
||||
return request('/api/workspace/ai-assistant/send-message', data, 'POST');
|
||||
}
|
||||
|
||||
// 获取AI分析报告
|
||||
export function getAIAnalysisReport() {
|
||||
return request('/api/workspace/ai-assistant/analysis-report', undefined, 'GET');
|
||||
}
|
||||
|
||||
// ==================== 订单相关接口 ====================
|
||||
|
||||
// 获取订单列表
|
||||
export function getOrders(params: any) {
|
||||
return request('/api/orders', params, 'GET');
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
export function getOrderDetail(orderId: string) {
|
||||
return request(`/api/orders/${orderId}`, undefined, 'GET');
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
export function createOrder(data: any) {
|
||||
return request('/api/orders', data, 'POST');
|
||||
}
|
||||
|
||||
// 更新订单
|
||||
export function updateOrder(orderId: string, data: any) {
|
||||
return request(`/api/orders/${orderId}`, data, 'PUT');
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
export function deleteOrder(orderId: string) {
|
||||
return request(`/api/orders/${orderId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 用户相关接口 ====================
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return request('/api/user/info', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
export function updateUserInfo(data: any) {
|
||||
return request('/api/user/info', data, 'PUT');
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export function changePassword(data: any) {
|
||||
return request('/api/user/change-password', data, 'POST');
|
||||
}
|
||||
|
||||
// 上传头像
|
||||
export function uploadAvatar(data: any) {
|
||||
return request('/api/user/upload-avatar', data, 'POST');
|
||||
}
|
||||
|
||||
// ==================== 文件上传相关接口 ====================
|
||||
|
||||
// 上传文件
|
||||
export function uploadFile(data: any) {
|
||||
return request('/api/upload/file', data, 'POST');
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
export function uploadImage(data: any) {
|
||||
return request('/api/upload/image', data, 'POST');
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
export function deleteFile(fileId: string) {
|
||||
return request(`/api/upload/files/${fileId}`, undefined, 'DELETE');
|
||||
}
|
||||
|
||||
// ==================== 系统配置相关接口 ====================
|
||||
|
||||
// 获取系统配置
|
||||
export function getSystemConfig() {
|
||||
return request('/api/system/config', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 更新系统配置
|
||||
export function updateSystemConfig(data: any) {
|
||||
return request('/api/system/config', data, 'PUT');
|
||||
}
|
||||
|
||||
// 获取系统通知
|
||||
export function getSystemNotifications() {
|
||||
return request('/api/system/notifications', undefined, 'GET');
|
||||
}
|
||||
|
||||
// 标记通知为已读
|
||||
export function markNotificationAsRead(notificationId: string) {
|
||||
return request(`/api/system/notifications/${notificationId}/read`, undefined, 'PUT');
|
||||
}
|
||||
10
nkebao/src/pages/traffic-pool/TrafficPool.tsx
Normal file
10
nkebao/src/pages/traffic-pool/TrafficPool.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const TrafficPool: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="流量池" showAddButton addButtonText="新建流量池" />
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficPool;
|
||||
8
nkebao/src/pages/traffic-pool/TrafficPoolDetail.tsx
Normal file
8
nkebao/src/pages/traffic-pool/TrafficPoolDetail.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const TrafficPoolDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="流量池详情" />;
|
||||
};
|
||||
|
||||
export default TrafficPoolDetail;
|
||||
30
nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx
Normal file
30
nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
37
nkebao/src/pages/wechat-accounts/WechatAccounts.tsx
Normal file
37
nkebao/src/pages/wechat-accounts/WechatAccounts.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
8
nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx
Normal file
8
nkebao/src/pages/workspace/ai-assistant/AIAssistant.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const AIAssistant: React.FC = () => {
|
||||
return <PlaceholderPage title="AI助手" showBack={false} />;
|
||||
};
|
||||
|
||||
export default AIAssistant;
|
||||
10
nkebao/src/pages/workspace/auto-group/AutoGroup.tsx
Normal file
10
nkebao/src/pages/workspace/auto-group/AutoGroup.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const AutoGroup: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="自动分组" showAddButton addButtonText="新建分组" />
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoGroup;
|
||||
8
nkebao/src/pages/workspace/auto-group/Detail.tsx
Normal file
8
nkebao/src/pages/workspace/auto-group/Detail.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const AutoGroupDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="自动分组详情" />;
|
||||
};
|
||||
|
||||
export default AutoGroupDetail;
|
||||
38
nkebao/src/pages/workspace/auto-like/AutoLike.tsx
Normal file
38
nkebao/src/pages/workspace/auto-like/AutoLike.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
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 AutoLike: React.FC = () => {
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar
|
||||
backArrow
|
||||
style={{ background: "#fff" }}
|
||||
onBack={() => window.history.back()}
|
||||
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 AutoLike;
|
||||
8
nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx
Normal file
8
nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const AutoLikeDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="自动点赞详情" />;
|
||||
};
|
||||
|
||||
export default AutoLikeDetail;
|
||||
30
nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx
Normal file
30
nkebao/src/pages/workspace/auto-like/NewAutoLike.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { NavBar } from "antd-mobile";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
|
||||
const NewAutoLike: 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 NewAutoLike;
|
||||
10
nkebao/src/pages/workspace/group-push/GroupPush.tsx
Normal file
10
nkebao/src/pages/workspace/group-push/GroupPush.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const GroupPush: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="群发推送" showAddButton addButtonText="新建推送" />
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupPush;
|
||||
8
nkebao/src/pages/workspace/group-push/new.tsx
Normal file
8
nkebao/src/pages/workspace/group-push/new.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewGroupPush: React.FC = () => {
|
||||
return <PlaceholderPage title="新建群发推送" />;
|
||||
};
|
||||
|
||||
export default NewGroupPush;
|
||||
14
nkebao/src/pages/workspace/main/api.ts
Normal file
14
nkebao/src/pages/workspace/main/api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import request from '@/api/request';
|
||||
|
||||
// 设备统计
|
||||
export function getDeviceStats() {
|
||||
return request('/v1/dashboard/device-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 微信号统计
|
||||
export function getWechatStats() {
|
||||
return request('/v1/dashboard/wechat-stats', {}, 'GET');
|
||||
}
|
||||
|
||||
// 你可以根据需要继续添加其他接口
|
||||
// 例如:场景获客统计、今日数据统计等
|
||||
119
nkebao/src/pages/workspace/main/index.module.scss
Normal file
119
nkebao/src/pages/workspace/main/index.module.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
.workspace {
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.featuresGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.featureLink {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.featureCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.adm-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.featureHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.featureName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.newBadge {
|
||||
margin-left: 8px;
|
||||
|
||||
:global(.adm-badge-content) {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.featureDescription {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 375px) {
|
||||
.workspace {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.featuresGrid {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.featureCard {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
214
nkebao/src/pages/workspace/main/index.tsx
Normal file
214
nkebao/src/pages/workspace/main/index.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Card, NavBar, Badge } from "antd-mobile";
|
||||
import {
|
||||
LikeOutlined,
|
||||
MessageOutlined,
|
||||
SendOutlined,
|
||||
TeamOutlined,
|
||||
LinkOutlined,
|
||||
AppstoreOutlined,
|
||||
PieChartOutlined,
|
||||
BarChartOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const Workspace: React.FC = () => {
|
||||
// 常用功能
|
||||
const commonFeatures = [
|
||||
{
|
||||
id: "auto-like",
|
||||
name: "自动点赞",
|
||||
description: "智能自动点赞互动",
|
||||
icon: (
|
||||
<LikeOutlined className={styles.icon} style={{ color: "#ff4d4f" }} />
|
||||
),
|
||||
path: "/workspace/auto-like",
|
||||
bgColor: "#fff2f0",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "moments-sync",
|
||||
name: "朋友圈同步",
|
||||
description: "自动同步朋友圈内容",
|
||||
icon: (
|
||||
<ClockCircleOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#722ed1" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/moments-sync",
|
||||
bgColor: "#f9f0ff",
|
||||
},
|
||||
{
|
||||
id: "group-push",
|
||||
name: "群消息推送",
|
||||
description: "智能群发助手",
|
||||
icon: (
|
||||
<SendOutlined className={styles.icon} style={{ color: "#fa8c16" }} />
|
||||
),
|
||||
path: "/workspace/group-push",
|
||||
bgColor: "#fff7e6",
|
||||
},
|
||||
{
|
||||
id: "auto-group",
|
||||
name: "自动建群",
|
||||
description: "智能拉好友建群",
|
||||
icon: (
|
||||
<TeamOutlined className={styles.icon} style={{ color: "#52c41a" }} />
|
||||
),
|
||||
path: "/workspace/auto-group",
|
||||
bgColor: "#f6ffed",
|
||||
},
|
||||
{
|
||||
id: "traffic-distribution",
|
||||
name: "流量分发",
|
||||
description: "管理流量分发和分配",
|
||||
icon: (
|
||||
<LinkOutlined className={styles.icon} style={{ color: "#1890ff" }} />
|
||||
),
|
||||
path: "/workspace/traffic-distribution",
|
||||
bgColor: "#e6f7ff",
|
||||
},
|
||||
{
|
||||
id: "ai-assistant",
|
||||
name: "AI对话助手",
|
||||
description: "智能回复,提高互动质量",
|
||||
icon: (
|
||||
<MessageOutlined className={styles.icon} style={{ color: "#1890ff" }} />
|
||||
),
|
||||
path: "/workspace/ai-assistant",
|
||||
bgColor: "#e6f7ff",
|
||||
isNew: true,
|
||||
},
|
||||
];
|
||||
|
||||
// AI智能助手
|
||||
const aiFeatures = [
|
||||
{
|
||||
id: "ai-analyzer",
|
||||
name: "AI数据分析",
|
||||
description: "智能分析客户行为特征",
|
||||
icon: (
|
||||
<BarChartOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#531dab" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/ai-analyzer",
|
||||
bgColor: "#f0f0ff",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-strategy",
|
||||
name: "AI策略优化",
|
||||
description: "智能优化获客策略",
|
||||
icon: (
|
||||
<AppstoreOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#13c2c2" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/ai-strategy",
|
||||
bgColor: "#e6fffb",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-forecast",
|
||||
name: "AI销售预测",
|
||||
description: "智能预测销售趋势",
|
||||
icon: (
|
||||
<PieChartOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#d48806" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/ai-forecast",
|
||||
bgColor: "#fffbe6",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
工作台
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
footer={<MeauMobile />}
|
||||
>
|
||||
<div className={styles.workspace}>
|
||||
{/* 常用功能 */}
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>常用功能</h2>
|
||||
<div className={styles.featuresGrid}>
|
||||
{commonFeatures.map((feature) => (
|
||||
<Link
|
||||
to={feature.path}
|
||||
key={feature.id}
|
||||
className={styles.featureLink}
|
||||
>
|
||||
<Card className={styles.featureCard}>
|
||||
<div
|
||||
className={styles.featureIcon}
|
||||
style={{ backgroundColor: feature.bgColor }}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className={styles.featureHeader}>
|
||||
<div className={styles.featureName}>{feature.name}</div>
|
||||
{feature.isNew && (
|
||||
<Badge content="New" className={styles.newBadge} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.featureDescription}>
|
||||
{feature.description}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI智能助手 */}
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>AI 智能助手</h2>
|
||||
<div className={styles.featuresGrid}>
|
||||
{aiFeatures.map((feature) => (
|
||||
<Link
|
||||
to={feature.path}
|
||||
key={feature.id}
|
||||
className={styles.featureLink}
|
||||
>
|
||||
<Card className={styles.featureCard}>
|
||||
<div
|
||||
className={styles.featureIcon}
|
||||
style={{ backgroundColor: feature.bgColor }}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className={styles.featureHeader}>
|
||||
<div className={styles.featureName}>{feature.name}</div>
|
||||
{feature.isNew && (
|
||||
<Badge content="New" className={styles.newBadge} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.featureDescription}>
|
||||
{feature.description}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Workspace;
|
||||
8
nkebao/src/pages/workspace/moments-sync/Detail.tsx
Normal file
8
nkebao/src/pages/workspace/moments-sync/Detail.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const MomentsSyncDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="朋友圈同步详情" />;
|
||||
};
|
||||
|
||||
export default MomentsSyncDetail;
|
||||
14
nkebao/src/pages/workspace/moments-sync/MomentsSync.tsx
Normal file
14
nkebao/src/pages/workspace/moments-sync/MomentsSync.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const MomentsSync: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage
|
||||
title="朋友圈同步"
|
||||
showAddButton
|
||||
addButtonText="新建同步"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MomentsSync;
|
||||
8
nkebao/src/pages/workspace/moments-sync/new.tsx
Normal file
8
nkebao/src/pages/workspace/moments-sync/new.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewMomentsSync: React.FC = () => {
|
||||
return <PlaceholderPage title="新建朋友圈同步" />;
|
||||
};
|
||||
|
||||
export default NewMomentsSync;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const TrafficDistributionDetail: React.FC = () => {
|
||||
return <PlaceholderPage title="流量分发详情" />;
|
||||
};
|
||||
|
||||
export default TrafficDistributionDetail;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const NewDistribution: React.FC = () => {
|
||||
return <PlaceholderPage title="新建流量分发" />;
|
||||
};
|
||||
|
||||
export default NewDistribution;
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import PlaceholderPage from "@/components/PlaceholderPage";
|
||||
|
||||
const TrafficDistribution: React.FC = () => {
|
||||
return (
|
||||
<PlaceholderPage title="流量分发" showAddButton addButtonText="新建分发" />
|
||||
);
|
||||
};
|
||||
|
||||
export default TrafficDistribution;
|
||||
1
nkebao/src/react-app-env.d.ts
vendored
Normal file
1
nkebao/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
171
nkebao/src/router/config.ts
Normal file
171
nkebao/src/router/config.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
// 路由配置类型定义
|
||||
export interface RouteConfig {
|
||||
path: string;
|
||||
element: React.ReactNode;
|
||||
auth: boolean;
|
||||
requiredRole?: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
children?: RouteConfig[];
|
||||
}
|
||||
|
||||
// 路由分组配置
|
||||
export const routeGroups = {
|
||||
// 基础路由
|
||||
basic: {
|
||||
name: "基础功能",
|
||||
routes: ["/", "/login", "/scene", "/work", "/mine"],
|
||||
},
|
||||
|
||||
// 设备管理
|
||||
devices: {
|
||||
name: "设备管理",
|
||||
routes: ["/devices", "/devices/:id"],
|
||||
},
|
||||
|
||||
// 微信号管理
|
||||
wechatAccounts: {
|
||||
name: "微信号管理",
|
||||
routes: ["/wechat-accounts", "/wechat-accounts/:id"],
|
||||
},
|
||||
|
||||
// 工作台
|
||||
workspace: {
|
||||
name: "工作台",
|
||||
routes: [
|
||||
"/workspace",
|
||||
"/workspace/auto-like",
|
||||
"/workspace/auto-like/new",
|
||||
"/workspace/auto-like/:id",
|
||||
"/workspace/auto-like/:id/edit",
|
||||
"/workspace/auto-group",
|
||||
"/workspace/auto-group/:id",
|
||||
"/workspace/group-push",
|
||||
"/workspace/group-push/new",
|
||||
"/workspace/group-push/:id",
|
||||
"/workspace/group-push/:id/edit",
|
||||
"/workspace/moments-sync",
|
||||
"/workspace/moments-sync/new",
|
||||
"/workspace/moments-sync/:id",
|
||||
"/workspace/moments-sync/edit/:id",
|
||||
"/workspace/ai-assistant",
|
||||
"/workspace/traffic-distribution",
|
||||
"/workspace/traffic-distribution/new",
|
||||
"/workspace/traffic-distribution/edit/:id",
|
||||
"/workspace/traffic-distribution/:id",
|
||||
],
|
||||
},
|
||||
|
||||
// 场景管理
|
||||
scenarios: {
|
||||
name: "场景管理",
|
||||
routes: [
|
||||
"/scenarios",
|
||||
"/scenarios/new",
|
||||
"/scenarios/new/:scenarioId",
|
||||
"/scenarios/edit/:planId",
|
||||
"/scenarios/list/:scenarioId/:scenarioName",
|
||||
],
|
||||
},
|
||||
|
||||
// 内容管理
|
||||
content: {
|
||||
name: "内容管理",
|
||||
routes: [
|
||||
"/content",
|
||||
"/content/new",
|
||||
"/content/edit/:id",
|
||||
"/content/materials/:id",
|
||||
"/content/materials/new/:id",
|
||||
"/content/materials/edit/:id/:materialId",
|
||||
],
|
||||
},
|
||||
|
||||
// 流量池
|
||||
trafficPool: {
|
||||
name: "流量池",
|
||||
routes: ["/traffic-pool", "/traffic-pool/:id"],
|
||||
},
|
||||
|
||||
// 其他功能
|
||||
other: {
|
||||
name: "其他功能",
|
||||
routes: [
|
||||
"/profile",
|
||||
"/plans",
|
||||
"/plans/:planId",
|
||||
"/orders",
|
||||
"/contact-import",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// 路由权限配置
|
||||
export const routePermissions = {
|
||||
// 管理员权限
|
||||
admin: Object.values(routeGroups).flatMap(group => group.routes),
|
||||
|
||||
// 普通用户权限
|
||||
user: [
|
||||
"/",
|
||||
"/login",
|
||||
"/scene",
|
||||
"/work",
|
||||
"/mine",
|
||||
"/devices",
|
||||
"/devices/:id",
|
||||
"/wechat-accounts",
|
||||
"/wechat-accounts/:id",
|
||||
"/workspace",
|
||||
"/scenarios",
|
||||
"/content",
|
||||
"/traffic-pool",
|
||||
"/traffic-pool/:id",
|
||||
"/profile",
|
||||
"/plans",
|
||||
"/plans/:planId",
|
||||
"/orders",
|
||||
"/contact-import",
|
||||
],
|
||||
|
||||
// 访客权限
|
||||
guest: ["/", "/login"],
|
||||
};
|
||||
|
||||
// 路由标题映射
|
||||
export const routeTitles: Record<string, string> = {
|
||||
"/": "首页",
|
||||
"/login": "登录",
|
||||
"/scene": "场景获客",
|
||||
"/work": "工作台",
|
||||
"/mine": "我的",
|
||||
"/devices": "设备管理",
|
||||
"/wechat-accounts": "微信号管理",
|
||||
"/workspace": "工作台",
|
||||
"/scenarios": "场景管理",
|
||||
"/content": "内容管理",
|
||||
"/traffic-pool": "流量池",
|
||||
"/profile": "个人中心",
|
||||
"/plans": "计划管理",
|
||||
"/orders": "订单管理",
|
||||
"/contact-import": "联系人导入",
|
||||
};
|
||||
|
||||
// 获取路由标题
|
||||
export const getRouteTitle = (path: string): string => {
|
||||
return routeTitles[path] || "页面";
|
||||
};
|
||||
|
||||
// 检查路由权限
|
||||
export const checkRoutePermission = (
|
||||
path: string,
|
||||
userRole: string = "user"
|
||||
): boolean => {
|
||||
const allowedRoutes = routePermissions[userRole as keyof typeof routePermissions] || [];
|
||||
return allowedRoutes.some(route => {
|
||||
// 简单的路径匹配,支持动态参数
|
||||
const routePattern = route.replace(/:[^/]+/g, "[^/]+");
|
||||
const regex = new RegExp(`^${routePattern}$`);
|
||||
return regex.test(path);
|
||||
});
|
||||
};
|
||||
49
nkebao/src/router/index.tsx
Normal file
49
nkebao/src/router/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter, useRoutes, RouteObject } from "react-router-dom";
|
||||
import PermissionRoute from "./permissionRoute";
|
||||
|
||||
// 动态导入所有 module 下的 ts/tsx 路由模块
|
||||
const modules = import.meta.glob("./module/*.{ts,tsx}", { eager: true });
|
||||
|
||||
// 合并所有模块的默认导出(假设每个模块都是 export default 路由数组)
|
||||
const allRoutes: (RouteObject & { auth?: boolean; requiredRole?: string })[] =
|
||||
[];
|
||||
Object.values(modules).forEach((mod: any) => {
|
||||
if (Array.isArray(mod.default)) {
|
||||
allRoutes.push(...mod.default);
|
||||
}
|
||||
});
|
||||
|
||||
// 权限包装
|
||||
function wrapWithPermission(
|
||||
route: RouteObject & { auth?: boolean; requiredRole?: string }
|
||||
) {
|
||||
if (route.auth) {
|
||||
return {
|
||||
...route,
|
||||
element: (
|
||||
<PermissionRoute requiredRole={route.requiredRole}>
|
||||
{route.element}
|
||||
</PermissionRoute>
|
||||
),
|
||||
};
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
const routes = allRoutes.map(wrapWithPermission);
|
||||
|
||||
const AppRoutes = () => useRoutes(routes);
|
||||
|
||||
const AppRouter: React.FC = () => (
|
||||
<BrowserRouter
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
}}
|
||||
>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default AppRouter;
|
||||
11
nkebao/src/router/module/auth.tsx
Normal file
11
nkebao/src/router/module/auth.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import Login from "@/pages/login/login";
|
||||
|
||||
const authRoutes = [
|
||||
{
|
||||
path: "/login",
|
||||
element: <Login />,
|
||||
auth: false, // 不需要权限
|
||||
},
|
||||
];
|
||||
|
||||
export default authRoutes;
|
||||
39
nkebao/src/router/module/content.tsx
Normal file
39
nkebao/src/router/module/content.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import Content from "@/pages/content/Content";
|
||||
import NewContent from "@/pages/content/NewContent";
|
||||
import Materials from "@/pages/content/materials/List";
|
||||
import MaterialsNew from "@/pages/content/materials/New";
|
||||
|
||||
const contentRoutes = [
|
||||
{
|
||||
path: "/content",
|
||||
element: <Content />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/content/new",
|
||||
element: <NewContent />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/content/edit/:id",
|
||||
element: <NewContent />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/content/materials/:id",
|
||||
element: <Materials />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/content/materials/new/:id",
|
||||
element: <MaterialsNew />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/content/materials/edit/:id/:materialId",
|
||||
element: <MaterialsNew />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default contentRoutes;
|
||||
17
nkebao/src/router/module/devices.tsx
Normal file
17
nkebao/src/router/module/devices.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import Devices from "@/pages/devices/Devices";
|
||||
import DeviceDetail from "@/pages/devices/DeviceDetail";
|
||||
|
||||
const deviceRoutes = [
|
||||
{
|
||||
path: "/devices",
|
||||
element: <Devices />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/devices/:id",
|
||||
element: <DeviceDetail />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default deviceRoutes;
|
||||
18
nkebao/src/router/module/index.tsx
Normal file
18
nkebao/src/router/module/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Home from "@/pages/home/index";
|
||||
import Mine from "@/pages/mine/index";
|
||||
|
||||
const routes = [
|
||||
// 基础路由
|
||||
{
|
||||
path: "/",
|
||||
element: <Home />,
|
||||
auth: true, // 需要登录
|
||||
},
|
||||
{
|
||||
path: "/mine",
|
||||
element: <Mine />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
40
nkebao/src/router/module/other.tsx
Normal file
40
nkebao/src/router/module/other.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Profile from "@/pages/profile/Profile";
|
||||
import Plans from "@/pages/plans/Plans";
|
||||
import PlanDetail from "@/pages/plans/PlanDetail";
|
||||
import Orders from "@/pages/orders/Orders";
|
||||
import ContactImport from "@/pages/contact-import/ContactImport";
|
||||
|
||||
const otherRoutes = [
|
||||
{
|
||||
path: "/mine",
|
||||
element: <Profile />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/profile",
|
||||
element: <Profile />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/plans",
|
||||
element: <Plans />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/plans/:planId",
|
||||
element: <PlanDetail />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/orders",
|
||||
element: <Orders />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/contact-import",
|
||||
element: <ContactImport />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default otherRoutes;
|
||||
33
nkebao/src/router/module/scenarios.tsx
Normal file
33
nkebao/src/router/module/scenarios.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import ScenariosList from "@/pages/scenarios/list";
|
||||
import NewPlan from "@/pages/scenarios/plan/new";
|
||||
import ListPlan from "@/pages/scenarios/plan/list";
|
||||
|
||||
const scenarioRoutes = [
|
||||
{
|
||||
path: "/scenarios",
|
||||
element: <ScenariosList />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/scenarios/new",
|
||||
element: <NewPlan />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/scenarios/new/:scenarioId",
|
||||
element: <NewPlan />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/scenarios/edit/:planId",
|
||||
element: <NewPlan />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/scenarios/list/:scenarioId/:scenarioName",
|
||||
element: <ListPlan />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default scenarioRoutes;
|
||||
17
nkebao/src/router/module/traffic-pool.tsx
Normal file
17
nkebao/src/router/module/traffic-pool.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import TrafficPool from "@/pages/traffic-pool/TrafficPool";
|
||||
import TrafficPoolDetail from "@/pages/traffic-pool/TrafficPoolDetail";
|
||||
|
||||
const trafficPoolRoutes = [
|
||||
{
|
||||
path: "/traffic-pool",
|
||||
element: <TrafficPool />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/traffic-pool/:id",
|
||||
element: <TrafficPoolDetail />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default trafficPoolRoutes;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user