feat: 本次提交更新内容如下
更新
This commit is contained in:
@@ -1,21 +1,30 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Bell, Smartphone, Users, Activity, MessageSquare, TrendingUp } from 'lucide-react';
|
import {
|
||||||
import Chart from 'chart.js/auto';
|
Bell,
|
||||||
import Layout from '@/components/Layout';
|
Smartphone,
|
||||||
import BottomNav from '@/components/BottomNav';
|
Users,
|
||||||
import UnifiedHeader, { HeaderPresets } from '@/components/UnifiedHeader';
|
Activity,
|
||||||
import { Card } from '@/components/ui/card';
|
MessageSquare,
|
||||||
import { Progress } from '@/components/ui/progress';
|
TrendingUp,
|
||||||
import '@/components/Layout.css';
|
} 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接口定义
|
// 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请求客户端
|
// 统一的API请求客户端
|
||||||
async function apiRequest<T>(url: string): Promise<T> {
|
async function apiRequest<T>(url: string): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
const token =
|
||||||
|
typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
@@ -99,7 +108,7 @@ export default function Home() {
|
|||||||
growth: 12,
|
growth: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "xiaohongshu",
|
id: "xiaohongshu",
|
||||||
name: "小红书获客",
|
name: "小红书获客",
|
||||||
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
icon: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-yvnMxpoBUzcvEkr8DfvHgPHEo1kmQ3.png",
|
||||||
color: "bg-red-100 text-red-600",
|
color: "bg-red-100 text-red-600",
|
||||||
@@ -135,7 +144,7 @@ export default function Home() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "群发任务",
|
title: "群发任务",
|
||||||
value: "8",
|
value: "8",
|
||||||
icon: <Users className="h-4 w-4" />,
|
icon: <Users className="h-4 w-4" />,
|
||||||
color: "text-orange-600",
|
color: "text-orange-600",
|
||||||
path: "/workspace/group-push",
|
path: "/workspace/group-push",
|
||||||
@@ -180,10 +189,11 @@ export default function Home() {
|
|||||||
// 尝试请求API数据
|
// 尝试请求API数据
|
||||||
try {
|
try {
|
||||||
// 并行请求多个接口
|
// 并行请求多个接口
|
||||||
const [deviceStatsResult, wechatStatsResult] = await Promise.allSettled([
|
const [deviceStatsResult, wechatStatsResult] =
|
||||||
apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`),
|
await Promise.allSettled([
|
||||||
apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`),
|
apiRequest(`${API_BASE_URL}/v1/dashboard/device-stats`),
|
||||||
]);
|
apiRequest(`${API_BASE_URL}/v1/dashboard/wechat-stats`),
|
||||||
|
]);
|
||||||
|
|
||||||
const newStats = {
|
const newStats = {
|
||||||
totalDevices: 0,
|
totalDevices: 0,
|
||||||
@@ -213,7 +223,9 @@ export default function Home() {
|
|||||||
setStats(newStats);
|
setStats(newStats);
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.warn("API请求失败,使用默认数据:", apiError);
|
console.warn("API请求失败,使用默认数据:", apiError);
|
||||||
setApiError(apiError instanceof Error ? apiError.message : "API连接失败");
|
setApiError(
|
||||||
|
apiError instanceof Error ? apiError.message : "API连接失败"
|
||||||
|
);
|
||||||
|
|
||||||
// 使用默认数据
|
// 使用默认数据
|
||||||
setStats({
|
setStats({
|
||||||
@@ -247,11 +259,11 @@ export default function Home() {
|
|||||||
}, []); // 移除stats依赖
|
}, []); // 移除stats依赖
|
||||||
|
|
||||||
const handleDevicesClick = () => {
|
const handleDevicesClick = () => {
|
||||||
navigate('/profile/devices');
|
navigate("/profile/devices");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWechatClick = () => {
|
const handleWechatClick = () => {
|
||||||
navigate('/wechat-accounts');
|
navigate("/wechat-accounts");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用Chart.js创建图表
|
// 使用Chart.js创建图表
|
||||||
@@ -263,7 +275,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ctx = chartRef.current.getContext("2d");
|
const ctx = chartRef.current.getContext("2d");
|
||||||
|
|
||||||
// 添加null检查
|
// 添加null检查
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
@@ -391,9 +403,12 @@ export default function Home() {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-gray-500 mb-1">设备数量</span>
|
<span className="text-xs text-gray-500 mb-1">设备数量</span>
|
||||||
<div className="flex items-center justify-between">
|
<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" />
|
<Smartphone className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,22 +417,31 @@ export default function Home() {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-gray-500 mb-1">微信号数量</span>
|
<span className="text-xs text-gray-500 mb-1">微信号数量</span>
|
||||||
<div className="flex items-center justify-between">
|
<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" />
|
<Users className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-2"></div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Card className="p-3 bg-white">
|
<Card className="p-3 bg-white">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-xs text-gray-500 mb-1">在线微信号</span>
|
<span className="text-xs text-gray-500 mb-1">在线微信号</span>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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" />
|
<Activity className="w-5 h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={
|
value={
|
||||||
stats.totalWechatAccounts > 0 ? (stats.onlineWechatAccounts / stats.totalWechatAccounts) * 100 : 0
|
stats.totalWechatAccounts > 0
|
||||||
|
? (stats.onlineWechatAccounts /
|
||||||
|
stats.totalWechatAccounts) *
|
||||||
|
100
|
||||||
|
: 0
|
||||||
}
|
}
|
||||||
className="h-1"
|
className="h-1"
|
||||||
/>
|
/>
|
||||||
@@ -435,16 +459,30 @@ export default function Home() {
|
|||||||
.sort((a, b) => b.value - a.value)
|
.sort((a, b) => b.value - a.value)
|
||||||
.slice(0, 4) // 只显示前4个
|
.slice(0, 4) // 只显示前4个
|
||||||
.map((scenario) => (
|
.map((scenario) => (
|
||||||
<div
|
<div
|
||||||
key={scenario.id}
|
key={scenario.id}
|
||||||
className="block flex-1 cursor-pointer"
|
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="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`}>
|
<div
|
||||||
<img src={scenario.icon || "/placeholder.svg"} alt={scenario.name} className="w-5 h-5" />
|
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>
|
||||||
<div className="text-sm font-medium">{scenario.value}</div>
|
|
||||||
<div className="text-xs text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis w-full">
|
<div className="text-xs text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis w-full">
|
||||||
{scenario.name}
|
{scenario.name}
|
||||||
</div>
|
</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"
|
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)}
|
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>
|
||||||
<div className="text-lg font-semibold">{stat.value}</div>
|
<div className="text-lg font-semibold">{stat.value}</div>
|
||||||
<div className="text-xs text-gray-500">{stat.title}</div>
|
<div className="text-xs text-gray-500">{stat.title}</div>
|
||||||
@@ -487,4 +527,4 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
14
nkebao.code-workspace
Normal file
14
nkebao.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "nkebao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Cunkebao"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../MySelf/好版登项目/好版登小程序"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -199,10 +199,6 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #888;
|
color: #888;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 6px;
|
|
||||||
line-height: 1.4;
|
|
||||||
min-height: 32px;
|
|
||||||
max-height: 32px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
@@ -262,9 +258,6 @@
|
|||||||
|
|
||||||
// 响应式设计
|
// 响应式设计
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.scene-page {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenario-card {
|
.scenario-card {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
@@ -307,20 +300,18 @@
|
|||||||
padding: 12px 4px 10px 4px;
|
padding: 12px 4px 10px 4px;
|
||||||
}
|
}
|
||||||
.card-img-bg {
|
.card-img-bg {
|
||||||
width: 40px;
|
width: 60px;
|
||||||
height: 40px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.card-img {
|
.card-img {
|
||||||
width: 26px;
|
width: 40px;
|
||||||
height: 26px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.card-desc {
|
.card-desc {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
min-height: 24px;
|
|
||||||
max-height: 24px;
|
|
||||||
}
|
}
|
||||||
.card-count {
|
.card-count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { NavBar, Button, Toast } from "antd-mobile";
|
import { NavBar, Button, Toast } from "antd-mobile";
|
||||||
import { PlusOutlined, UpOutlined } from "@ant-design/icons";
|
import { PlusOutlined, RiseOutlined } from "@ant-design/icons";
|
||||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import { getScenarios } from "./api";
|
import { getScenarios } from "./api";
|
||||||
@@ -162,7 +162,7 @@ const Scene: React.FC = () => {
|
|||||||
今日: {scenario.count}
|
今日: {scenario.count}
|
||||||
</span>
|
</span>
|
||||||
<span className={style["card-growth"]}>
|
<span className={style["card-growth"]}>
|
||||||
<UpOutlined
|
<RiseOutlined
|
||||||
style={{ fontSize: 14, color: "#52c41a", marginRight: 2 }}
|
style={{ fontSize: 14, color: "#52c41a", marginRight: 2 }}
|
||||||
/>
|
/>
|
||||||
{scenario.growth}
|
{scenario.growth}
|
||||||
|
|||||||
22
nkebao/src/pages/scenarios/plan/list/data.ts
Normal file
22
nkebao/src/pages/scenarios/plan/list/data.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiSettings {
|
||||||
|
apiKey: string;
|
||||||
|
webhookUrl: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
@@ -41,22 +41,12 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.adm-input {
|
.ant-input {
|
||||||
padding-left: 40px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: #999;
|
|
||||||
font-size: 16px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-btn {
|
.refresh-btn {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -87,7 +77,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-name {
|
.plan-name {
|
||||||
@@ -98,21 +88,64 @@
|
|||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-meta {
|
.plan-header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-item {
|
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #999;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -120,12 +153,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -147,6 +174,48 @@
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
.api-dialog {
|
.api-dialog {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
@@ -188,7 +257,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adm-input {
|
.ant-input {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +267,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.adm-input {
|
.ant-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
Toast,
|
Toast,
|
||||||
SpinLoading,
|
SpinLoading,
|
||||||
Dialog,
|
Dialog,
|
||||||
Input,
|
|
||||||
Popup,
|
Popup,
|
||||||
Card,
|
Card,
|
||||||
Tag,
|
Tag,
|
||||||
Space,
|
Space,
|
||||||
} from "antd-mobile";
|
} from "antd-mobile";
|
||||||
|
import { Input } from "antd";
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -24,6 +24,8 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
QrcodeOutlined,
|
QrcodeOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
@@ -36,40 +38,7 @@ import {
|
|||||||
getWxMinAppCode,
|
getWxMinAppCode,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
|
import { Task, ApiSettings } from "./data";
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScenarioData {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
image: string;
|
|
||||||
description: string;
|
|
||||||
totalPlans: number;
|
|
||||||
totalCustomers: number;
|
|
||||||
todayCustomers: number;
|
|
||||||
growth: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiSettings {
|
|
||||||
apiKey: string;
|
|
||||||
webhookUrl: string;
|
|
||||||
taskId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScenarioList: React.FC = () => {
|
const ScenarioList: React.FC = () => {
|
||||||
const { scenarioId, scenarioName } = useParams<{
|
const { scenarioId, scenarioName } = useParams<{
|
||||||
@@ -78,10 +47,9 @@ const ScenarioList: React.FC = () => {
|
|||||||
}>();
|
}>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [scenario, setScenario] = useState<ScenarioData | null>(null);
|
const [pageTitle, setPageTitle] = useState<string>("");
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [showApiDialog, setShowApiDialog] = useState(false);
|
const [showApiDialog, setShowApiDialog] = useState(false);
|
||||||
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
|
const [currentApiSettings, setCurrentApiSettings] = useState<ApiSettings>({
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
@@ -93,6 +61,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
const [showQrDialog, setShowQrDialog] = useState(false);
|
const [showQrDialog, setShowQrDialog] = useState(false);
|
||||||
const [qrLoading, setQrLoading] = useState(false);
|
const [qrLoading, setQrLoading] = useState(false);
|
||||||
const [qrImg, setQrImg] = useState("");
|
const [qrImg, setQrImg] = useState("");
|
||||||
|
const [showActionMenu, setShowActionMenu] = useState<string | null>(null);
|
||||||
|
|
||||||
// 获取渠道中文名称
|
// 获取渠道中文名称
|
||||||
const getChannelName = (channel: string) => {
|
const getChannelName = (channel: string) => {
|
||||||
@@ -111,27 +80,19 @@ const ScenarioList: React.FC = () => {
|
|||||||
return channelMap[channel] || `${channel}获客`;
|
return channelMap[channel] || `${channel}获客`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取场景描述
|
// 获取场景名称
|
||||||
const getScenarioDescription = (channel: string) => {
|
const getScenarioName = useCallback(() => {
|
||||||
const descriptions: Record<string, string> = {
|
const urlName = searchParams.get("name");
|
||||||
douyin: "通过抖音平台进行精准获客,利用短视频内容吸引目标用户",
|
if (urlName) {
|
||||||
xiaohongshu: "利用小红书平台进行内容营销获客,通过优质内容建立品牌形象",
|
return urlName;
|
||||||
gongzhonghao: "通过微信公众号进行获客,建立私域流量池",
|
}
|
||||||
haibao: "通过海报分享进行获客,快速传播品牌信息",
|
return getChannelName(scenarioId || "");
|
||||||
phone: "通过电话营销进行获客,直接与客户沟通",
|
}, [searchParams, scenarioId]);
|
||||||
weixinqun: "通过微信群进行获客,利用社交裂变效应",
|
|
||||||
payment: "通过付款码进行获客,便捷的支付方式",
|
|
||||||
api: "通过API接口进行获客,支持第三方系统集成",
|
|
||||||
};
|
|
||||||
return descriptions[channel] || "通过该平台进行获客";
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchScenarioData = async () => {
|
const fetchScenarioData = async () => {
|
||||||
if (!scenarioId) return;
|
if (!scenarioId) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取计划列表
|
// 获取计划列表
|
||||||
@@ -142,98 +103,37 @@ const ScenarioList: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 设置计划列表
|
// 设置计划列表
|
||||||
if (response && response.data && response.data.list) {
|
setTasks(response.list);
|
||||||
setTasks(response.data.list);
|
|
||||||
} else {
|
|
||||||
setTasks([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建场景数据
|
// 设置页面标题
|
||||||
const scenarioData: ScenarioData = {
|
setPageTitle(getScenarioName());
|
||||||
id: scenarioId,
|
|
||||||
name: scenarioName || "",
|
|
||||||
image: "",
|
|
||||||
description: getScenarioDescription(scenarioId),
|
|
||||||
totalPlans: response?.data?.list?.length || 0,
|
|
||||||
totalCustomers: 0,
|
|
||||||
todayCustomers: 0,
|
|
||||||
growth: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
setScenario(scenarioData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取场景数据失败:", error);
|
console.error("获取场景数据失败:", error);
|
||||||
// 即使API失败也要创建基本的场景数据
|
|
||||||
const scenarioData: ScenarioData = {
|
|
||||||
id: scenarioId,
|
|
||||||
name: getScenarioName(),
|
|
||||||
image: "",
|
|
||||||
description: getScenarioDescription(scenarioId),
|
|
||||||
totalPlans: 0,
|
|
||||||
totalCustomers: 0,
|
|
||||||
todayCustomers: 0,
|
|
||||||
growth: "",
|
|
||||||
};
|
|
||||||
setScenario(scenarioData);
|
|
||||||
setTasks([]);
|
setTasks([]);
|
||||||
|
setPageTitle(getScenarioName());
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchScenarioData();
|
fetchScenarioData();
|
||||||
}, [scenarioId]);
|
}, [scenarioId, getScenarioName]);
|
||||||
|
|
||||||
// 获取场景名称
|
// 更新页面标题
|
||||||
const getScenarioName = useCallback(() => {
|
|
||||||
const urlName = searchParams.get("name");
|
|
||||||
if (urlName) {
|
|
||||||
return urlName;
|
|
||||||
}
|
|
||||||
return getChannelName(scenarioId || "");
|
|
||||||
}, [searchParams, scenarioId]);
|
|
||||||
|
|
||||||
// 更新场景数据中的名称
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setScenario((prev) =>
|
setPageTitle(getScenarioName());
|
||||||
prev
|
}, [getScenarioName]);
|
||||||
? {
|
|
||||||
...prev,
|
|
||||||
name: (() => {
|
|
||||||
const urlName = searchParams.get("name");
|
|
||||||
if (urlName) return urlName;
|
|
||||||
return getChannelName(scenarioId || "");
|
|
||||||
})(),
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
}, [searchParams, scenarioId]);
|
|
||||||
|
|
||||||
const handleCopyPlan = async (taskId: string) => {
|
const handleCopyPlan = async (taskId: string) => {
|
||||||
const taskToCopy = tasks.find((task) => task.id === taskId);
|
const taskToCopy = tasks.find((task) => task.id === taskId);
|
||||||
if (!taskToCopy) return;
|
if (!taskToCopy) return;
|
||||||
|
await copyPlan(taskId);
|
||||||
try {
|
Toast.show({
|
||||||
const response = await copyPlan(taskId);
|
content: `已成功复制"${taskToCopy.name}"`,
|
||||||
if (response && response.code === 200) {
|
position: "top",
|
||||||
Toast.show({
|
});
|
||||||
content: `已成功复制"${taskToCopy.name}"`,
|
// 刷新列表
|
||||||
position: "top",
|
handleRefresh();
|
||||||
});
|
|
||||||
// 刷新列表
|
|
||||||
handleRefresh();
|
|
||||||
} else {
|
|
||||||
Toast.show({
|
|
||||||
content: response?.msg || "复制失败",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Toast.show({
|
|
||||||
content: "复制失败,请重试",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePlan = async (taskId: string) => {
|
const handleDeletePlan = async (taskId: string) => {
|
||||||
@@ -343,7 +243,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
const getStatusText = (status: number) => {
|
const getStatusText = (status: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return "运行中";
|
return "进行中";
|
||||||
case 0:
|
case 0:
|
||||||
return "已暂停";
|
return "已暂停";
|
||||||
case -1:
|
case -1:
|
||||||
@@ -357,13 +257,11 @@ const ScenarioList: React.FC = () => {
|
|||||||
setLoadingTasks(true);
|
setLoadingTasks(true);
|
||||||
try {
|
try {
|
||||||
const response = await getPlanList({
|
const response = await getPlanList({
|
||||||
scenarioId: scenarioId!,
|
sceneId: scenarioId!,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
});
|
});
|
||||||
if (response && response.data && response.data.list) {
|
setTasks(response.list);
|
||||||
setTasks(response.data.list);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: "刷新失败",
|
content: "刷新失败",
|
||||||
@@ -378,6 +276,77 @@ const ScenarioList: React.FC = () => {
|
|||||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 计算通过率
|
||||||
|
const calculatePassRate = (acquired: number, added: number) => {
|
||||||
|
if (added === 0) return "0.00%";
|
||||||
|
return `${((acquired / added) * 100).toFixed(2)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timeString: string) => {
|
||||||
|
if (!timeString) return "";
|
||||||
|
const date = new Date(timeString);
|
||||||
|
return date
|
||||||
|
.toLocaleString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
.replace(/\//g, "-");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成操作菜单
|
||||||
|
const getActionMenu = (task: Task) => [
|
||||||
|
{
|
||||||
|
key: "edit",
|
||||||
|
text: "编辑",
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
setShowActionMenu(null);
|
||||||
|
navigate(`/scenarios/edit/${task.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "settings",
|
||||||
|
text: "API设置",
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
setShowActionMenu(null);
|
||||||
|
handleOpenApiSettings(task.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "copy",
|
||||||
|
text: "复制",
|
||||||
|
icon: <CopyOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
setShowActionMenu(null);
|
||||||
|
handleCopyPlan(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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
@@ -402,7 +371,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
<NavBar
|
<NavBar
|
||||||
back={null}
|
back={null}
|
||||||
style={{ background: "#fff" }}
|
style={{ background: "#fff" }}
|
||||||
left={<div className={style["nav-title"]}>{scenario?.name}</div>}
|
left={<div className={style["nav-title"]}>{pageTitle}</div>}
|
||||||
right={
|
right={
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -421,12 +390,13 @@ const ScenarioList: React.FC = () => {
|
|||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
<div className={style["search-bar"]}>
|
<div className={style["search-bar"]}>
|
||||||
<div className={style["search-input-wrapper"]}>
|
<div className={style["search-input-wrapper"]}>
|
||||||
<SearchOutlined className={style["search-icon"]} />
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索计划名称"
|
placeholder="搜索计划名称"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={setSearchTerm}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
clearable
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="middle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -457,63 +427,59 @@ const ScenarioList: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
filteredTasks.map((task) => (
|
filteredTasks.map((task) => (
|
||||||
<Card key={task.id} className={style["plan-item"]}>
|
<Card key={task.id} className={style["plan-item"]}>
|
||||||
{/* 头部:标题和状态 */}
|
{/* 头部:标题、状态和操作菜单 */}
|
||||||
<div className={style["plan-header"]}>
|
<div className={style["plan-header"]}>
|
||||||
<div className={style["plan-name"]}>{task.name}</div>
|
<div className={style["plan-name"]}>{task.name}</div>
|
||||||
<Tag color={getStatusColor(task.status)}>
|
<div className={style["plan-header-right"]}>
|
||||||
{getStatusText(task.status)}
|
<Tag color={getStatusColor(task.status)}>
|
||||||
</Tag>
|
{getStatusText(task.status)}
|
||||||
|
</Tag>
|
||||||
|
<Button
|
||||||
|
size="mini"
|
||||||
|
fill="none"
|
||||||
|
className={style["more-btn"]}
|
||||||
|
onClick={() => setShowActionMenu(task.id)}
|
||||||
|
>
|
||||||
|
<MoreOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 中部:更新时间和统计 */}
|
{/* 统计数据网格 */}
|
||||||
<div className={style["plan-meta"]}>
|
<div className={style["stats-grid"]}>
|
||||||
<div className={style["meta-item"]}>
|
<div className={style["stat-item"]}>
|
||||||
<CalendarOutlined />
|
<div className={style["stat-label"]}>设备数</div>
|
||||||
<span>最后更新: {task.updated_at || task.created_at}</span>
|
<div className={style["stat-value"]}>
|
||||||
|
{task.stats?.devices || 0}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["meta-item"]}>
|
<div className={style["stat-item"]}>
|
||||||
<UserOutlined />
|
<div className={style["stat-label"]}>已获客</div>
|
||||||
<span>
|
<div className={style["stat-value"]}>
|
||||||
设备: {task.stats?.devices || 0} | 获客:{" "}
|
{task.stats?.acquired || 0}
|
||||||
{task.stats?.acquired || 0} | 添加:{" "}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["stat-item"]}>
|
||||||
|
<div className={style["stat-label"]}>已添加</div>
|
||||||
|
<div className={style["stat-value"]}>
|
||||||
{task.stats?.added || 0}
|
{task.stats?.added || 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>
|
||||||
|
上次执行: {formatTime(task.updated_at || task.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部:操作按钮 */}
|
|
||||||
<div className={style["plan-actions"]}>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
size="mini"
|
|
||||||
onClick={() => navigate(`/scenarios/edit/${task.id}`)}
|
|
||||||
>
|
|
||||||
<EditOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="mini"
|
|
||||||
onClick={() => handleOpenApiSettings(task.id)}
|
|
||||||
>
|
|
||||||
<SettingOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button size="mini" onClick={() => handleCopyPlan(task.id)}>
|
|
||||||
<CopyOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="mini"
|
|
||||||
color="danger"
|
|
||||||
onClick={() => handleDeletePlan(task.id)}
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="mini"
|
|
||||||
onClick={() => handleShowQrCode(task.id)}
|
|
||||||
>
|
|
||||||
<QrcodeOutlined />
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -537,7 +503,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
<div className={style["api-item"]}>
|
<div className={style["api-item"]}>
|
||||||
<label>API Key:</label>
|
<label>API Key:</label>
|
||||||
<div className={style["input-with-button"]}>
|
<div className={style["input-with-button"]}>
|
||||||
<Input value={currentApiSettings.apiKey} readOnly />
|
<Input value={currentApiSettings.apiKey} disabled />
|
||||||
<Button
|
<Button
|
||||||
size="mini"
|
size="mini"
|
||||||
onClick={() => handleCopyApiUrl(currentApiSettings.apiKey)}
|
onClick={() => handleCopyApiUrl(currentApiSettings.apiKey)}
|
||||||
@@ -549,7 +515,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
<div className={style["api-item"]}>
|
<div className={style["api-item"]}>
|
||||||
<label>Webhook URL:</label>
|
<label>Webhook URL:</label>
|
||||||
<div className={style["input-with-button"]}>
|
<div className={style["input-with-button"]}>
|
||||||
<Input value={currentApiSettings.webhookUrl} readOnly />
|
<Input value={currentApiSettings.webhookUrl} disabled />
|
||||||
<Button
|
<Button
|
||||||
size="mini"
|
size="mini"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -564,6 +530,38 @@ const ScenarioList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
|
{/* 操作菜单弹窗 */}
|
||||||
|
<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
|
<Popup
|
||||||
visible={showQrDialog}
|
visible={showQrDialog}
|
||||||
|
|||||||
Reference in New Issue
Block a user