|
@@ -4,16 +4,16 @@ import { useState, useEffect } from 'react';
|
|
|
import api from '@/lib/api';
|
|
import api from '@/lib/api';
|
|
|
import {
|
|
import {
|
|
|
Search, Loader2, Users, Clock, Percent,
|
|
Search, Loader2, Users, Clock, Percent,
|
|
|
- AlertTriangle, CheckCircle, ShieldAlert, X, Terminal, Lock
|
|
|
|
|
-} from 'lucide-react'; // 新增了 Lock 图标
|
|
|
|
|
|
|
+ AlertTriangle, CheckCircle, ShieldAlert, X, Terminal,
|
|
|
|
|
+ Lock, Settings2 // 引入了新功能的图标
|
|
|
|
|
+} from 'lucide-react';
|
|
|
|
|
|
|
|
-// 引入子组件
|
|
|
|
|
|
|
+// === 引入子组件 ===
|
|
|
import ProbabilityManager from '@/components/admin/slots/ProbabilityManager';
|
|
import ProbabilityManager from '@/components/admin/slots/ProbabilityManager';
|
|
|
import DailySlotDashboard from '@/components/admin/slots/DailySlotDashboard';
|
|
import DailySlotDashboard from '@/components/admin/slots/DailySlotDashboard';
|
|
|
import DashboardTaskPopup from '@/components/admin/slots/DashboardTaskPopup';
|
|
import DashboardTaskPopup from '@/components/admin/slots/DashboardTaskPopup';
|
|
|
-// === 新增引入 Session 管理组件 ===
|
|
|
|
|
import TroovSessionManager from '@/components/admin/slots/TroovSessionManager';
|
|
import TroovSessionManager from '@/components/admin/slots/TroovSessionManager';
|
|
|
-
|
|
|
|
|
|
|
+import TroovBookLimitManager from '@/components/admin/slots/TroovBookLimitManager'; // 引入名额配置组件
|
|
|
|
|
|
|
|
// Slot 容量数据类型
|
|
// Slot 容量数据类型
|
|
|
interface SlotItem {
|
|
interface SlotItem {
|
|
@@ -26,52 +26,62 @@ export default function AdminSlotsPage() {
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
// === 数据源状态 ===
|
|
// === 数据源状态 ===
|
|
|
- const [slots, setSlots] = useState<SlotItem[]>([]);
|
|
|
|
|
- const [grabbedTasks, setGrabbedTasks] = useState<any[]>([]);
|
|
|
|
|
|
|
+ const [slots, setSlots] = useState<SlotItem[]>([]); // 容量数据
|
|
|
|
|
+ const [grabbedTasks, setGrabbedTasks] = useState<any[]>([]); // 当天的任务数据
|
|
|
|
|
|
|
|
// 日期选择
|
|
// 日期选择
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
const [searchDate, setSearchDate] = useState(today);
|
|
const [searchDate, setSearchDate] = useState(today);
|
|
|
|
|
|
|
|
// === 功能面板开关 ===
|
|
// === 功能面板开关 ===
|
|
|
- const [showChecker, setShowChecker] = useState(false);
|
|
|
|
|
- const [showProbManager, setShowProbManager] = useState(false);
|
|
|
|
|
- // === 新增:锁单池管理面板开关 ===
|
|
|
|
|
- const [showSessionManager, setShowSessionManager] = useState(false);
|
|
|
|
|
|
|
+ const [showSessionManager, setShowSessionManager] = useState(false); // 锁单池管理
|
|
|
|
|
+ const [showLimitManager, setShowLimitManager] = useState(false); // 名额配置
|
|
|
|
|
+ const [showChecker, setShowChecker] = useState(false); // 冲突检测面板
|
|
|
|
|
+ const [showProbManager, setShowProbManager] = useState(false); // 概率管理面板
|
|
|
|
|
|
|
|
// === 冲突检测状态 ===
|
|
// === 冲突检测状态 ===
|
|
|
const [checkLoading, setCheckLoading] = useState(false);
|
|
const [checkLoading, setCheckLoading] = useState(false);
|
|
|
const [checkForm, setCheckForm] = useState({ first_name: '', last_name: '', birthday: '' });
|
|
const [checkForm, setCheckForm] = useState({ first_name: '', last_name: '', birthday: '' });
|
|
|
- const [apiResult, setApiResult] = useState<any>(null);
|
|
|
|
|
|
|
+ const [apiResult, setApiResult] = useState<any>(null); // 存储 API 原始响应
|
|
|
|
|
|
|
|
// === 任务详情弹窗状态 ===
|
|
// === 任务详情弹窗状态 ===
|
|
|
const [selectedTask, setSelectedTask] = useState<any>(null);
|
|
const [selectedTask, setSelectedTask] = useState<any>(null);
|
|
|
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
|
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
|
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
|
|
+ // 初始化加载 & 切换日期时加载
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
fetchData();
|
|
fetchData();
|
|
|
- }, [searchDate]); // 修改点:建议把 searchDate 放到依赖里,切换日期自动刷新
|
|
|
|
|
|
|
+ }, [searchDate]);
|
|
|
|
|
|
|
|
|
|
+ // === 核心数据获取逻辑 ===
|
|
|
const fetchData = async () => {
|
|
const fetchData = async () => {
|
|
|
if (!searchDate) return alert("请选择日期");
|
|
if (!searchDate) return alert("请选择日期");
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 并行请求:容量数据 + 任务数据
|
|
|
const ratePromise = api.get('/api/troov/rate', {
|
|
const ratePromise = api.get('/api/troov/rate', {
|
|
|
params: { date: searchDate }
|
|
params: { date: searchDate }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const taskPromise = api.get('/api/vas/task/list', {
|
|
const taskPromise = api.get('/api/vas/task/list', {
|
|
|
- params: { page: 1, size: 200, status: 'grabbed' }
|
|
|
|
|
|
|
+ params: {
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ size: 200,
|
|
|
|
|
+ status: 'grabbed'
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const [rateRes, taskRes] = await Promise.all([ratePromise, taskPromise]);
|
|
const [rateRes, taskRes] = await Promise.all([ratePromise, taskPromise]);
|
|
|
|
|
|
|
|
|
|
+ // 1. 处理 Slot 容量数据
|
|
|
const list = Array.isArray(rateRes.data.data) ? rateRes.data.data : (rateRes.data.data || []);
|
|
const list = Array.isArray(rateRes.data.data) ? rateRes.data.data : (rateRes.data.data || []);
|
|
|
|
|
+ // 按时间排序
|
|
|
list.sort((a: SlotItem, b: SlotItem) => a.time.localeCompare(b.time));
|
|
list.sort((a: SlotItem, b: SlotItem) => a.time.localeCompare(b.time));
|
|
|
setSlots(list);
|
|
setSlots(list);
|
|
|
|
|
|
|
|
|
|
+ // 2. 处理任务数据
|
|
|
const taskList = taskRes.data.data?.items || [];
|
|
const taskList = taskRes.data.data?.items || [];
|
|
|
setGrabbedTasks(taskList);
|
|
setGrabbedTasks(taskList);
|
|
|
|
|
|
|
@@ -84,30 +94,48 @@ export default function AdminSlotsPage() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // === 用户冲突检测逻辑 ===
|
|
|
const handleCheckUser = async (e: React.FormEvent) => {
|
|
const handleCheckUser = async (e: React.FormEvent) => {
|
|
|
- // ... 原有逻辑不变 ...
|
|
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
- if (!checkForm.first_name || !checkForm.last_name || !checkForm.birthday) return alert("请填写完整信息");
|
|
|
|
|
|
|
+ if (!checkForm.first_name || !checkForm.last_name || !checkForm.birthday) {
|
|
|
|
|
+ return alert("请填写完整信息");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
setCheckLoading(true);
|
|
setCheckLoading(true);
|
|
|
setApiResult(null);
|
|
setApiResult(null);
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const res = await api.post('/api/troov/book', checkForm);
|
|
const res = await api.post('/api/troov/book', checkForm);
|
|
|
- setApiResult({ status: res.status, statusText: res.statusText, data: res.data });
|
|
|
|
|
|
|
+ setApiResult({
|
|
|
|
|
+ status: res.status,
|
|
|
|
|
+ statusText: res.statusText,
|
|
|
|
|
+ data: res.data
|
|
|
|
|
+ });
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
- setApiResult({ status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data });
|
|
|
|
|
|
|
+ console.error(error);
|
|
|
|
|
+ setApiResult({
|
|
|
|
|
+ status: error.response?.status || 'Network Error',
|
|
|
|
|
+ statusText: error.response?.statusText || 'Failed',
|
|
|
|
|
+ data: error.response?.data || error.message
|
|
|
|
|
+ });
|
|
|
} finally {
|
|
} finally {
|
|
|
setCheckLoading(false);
|
|
setCheckLoading(false);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // === 任务操作处理 ===
|
|
|
const handleTaskClick = (task: any, target: HTMLElement) => {
|
|
const handleTaskClick = (task: any, target: HTMLElement) => {
|
|
|
- setAnchorEl(target);
|
|
|
|
|
|
|
+ setAnchorEl(target); // 保存锚点
|
|
|
setSelectedTask(task);
|
|
setSelectedTask(task);
|
|
|
setIsTaskModalOpen(true);
|
|
setIsTaskModalOpen(true);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const handleTaskSuccess = () => fetchData();
|
|
|
|
|
|
|
+ const handleTaskSuccess = () => {
|
|
|
|
|
+ // 任务操作成功(如标记完成)后,刷新数据
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
|
|
+ // === 统计计算 ===
|
|
|
const totalCapacity = slots.reduce((acc, curr) => acc + curr.capacity, 0);
|
|
const totalCapacity = slots.reduce((acc, curr) => acc + curr.capacity, 0);
|
|
|
const validSlots = slots.filter(s => s.capacity > 0 && Number(s.rate) <= 100).length;
|
|
const validSlots = slots.filter(s => s.capacity > 0 && Number(s.rate) <= 100).length;
|
|
|
const riskSlots = slots.filter(s => Number(s.rate) > 100).length;
|
|
const riskSlots = slots.filter(s => Number(s.rate) > 100).length;
|
|
@@ -122,9 +150,10 @@ export default function AdminSlotsPage() {
|
|
|
<p className="text-sm text-slate-500 mt-1">综合视图:容量风险 (Rate) + 抢单结果 (Grabbed)</p>
|
|
<p className="text-sm text-slate-500 mt-1">综合视图:容量风险 (Rate) + 抢单结果 (Grabbed)</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* 顶部按钮群 */}
|
|
|
<div className="flex flex-wrap gap-3 w-full xl:w-auto">
|
|
<div className="flex flex-wrap gap-3 w-full xl:w-auto">
|
|
|
|
|
|
|
|
- {/* === 新增:锁单池管理按钮 === */}
|
|
|
|
|
|
|
+ {/* 锁单池管理按钮 */}
|
|
|
<button
|
|
<button
|
|
|
onClick={() => setShowSessionManager(!showSessionManager)}
|
|
onClick={() => setShowSessionManager(!showSessionManager)}
|
|
|
className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition whitespace-nowrap
|
|
className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition whitespace-nowrap
|
|
@@ -132,7 +161,18 @@ export default function AdminSlotsPage() {
|
|
|
`}
|
|
`}
|
|
|
>
|
|
>
|
|
|
<Lock size={16} />
|
|
<Lock size={16} />
|
|
|
- {showSessionManager ? '关闭锁单池' : '锁单池管理'}
|
|
|
|
|
|
|
+ {showSessionManager ? '关闭锁单池' : '锁单池'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 名额限制配置按钮 */}
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setShowLimitManager(!showLimitManager)}
|
|
|
|
|
+ className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition whitespace-nowrap
|
|
|
|
|
+ ${showLimitManager ? 'bg-teal-50 border-teal-200 text-teal-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
|
|
|
|
|
+ `}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Settings2 size={16} />
|
|
|
|
|
+ {showLimitManager ? '关闭名额' : '名额配置'}
|
|
|
</button>
|
|
</button>
|
|
|
|
|
|
|
|
{/* 用户冲突检测开关 */}
|
|
{/* 用户冲突检测开关 */}
|
|
@@ -143,7 +183,7 @@ export default function AdminSlotsPage() {
|
|
|
`}
|
|
`}
|
|
|
>
|
|
>
|
|
|
<ShieldAlert size={16} />
|
|
<ShieldAlert size={16} />
|
|
|
- 冲突检测
|
|
|
|
|
|
|
+ {showChecker ? '关闭检测' : '冲突检测'}
|
|
|
</button>
|
|
</button>
|
|
|
|
|
|
|
|
{/* 概率管理开关 */}
|
|
{/* 概率管理开关 */}
|
|
@@ -154,7 +194,7 @@ export default function AdminSlotsPage() {
|
|
|
`}
|
|
`}
|
|
|
>
|
|
>
|
|
|
<Percent size={16} />
|
|
<Percent size={16} />
|
|
|
- 概率管理
|
|
|
|
|
|
|
+ {showProbManager ? '关闭概率' : '概率管理'}
|
|
|
</button>
|
|
</button>
|
|
|
|
|
|
|
|
{/* 日期选择与刷新 */}
|
|
{/* 日期选择与刷新 */}
|
|
@@ -179,28 +219,96 @@ export default function AdminSlotsPage() {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* === 2.1 新增:锁单池管理面板 === */}
|
|
|
|
|
|
|
+ {/* === 2. 动态插入的组件面板 === */}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 2.1 锁单池管理面板 */}
|
|
|
{showSessionManager && (
|
|
{showSessionManager && (
|
|
|
<div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
|
|
<div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
|
|
|
<TroovSessionManager />
|
|
<TroovSessionManager />
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {/* === 2.2 概率管理面板 === */}
|
|
|
|
|
|
|
+ {/* 2.2 预约名额配置面板 */}
|
|
|
|
|
+ {showLimitManager && (
|
|
|
|
|
+ <div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
|
|
|
|
|
+ <TroovBookLimitManager />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 2.3 概率管理面板 */}
|
|
|
{showProbManager && (
|
|
{showProbManager && (
|
|
|
<div className="mb-8 animate-in fade-in slide-in-from-top-4 duration-300">
|
|
<div className="mb-8 animate-in fade-in slide-in-from-top-4 duration-300">
|
|
|
<ProbabilityManager />
|
|
<ProbabilityManager />
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {/* === 3. 用户冲突检测面板 (原逻辑保持不变) === */}
|
|
|
|
|
|
|
+ {/* 2.4 用户冲突检测面板 */}
|
|
|
{showChecker && (
|
|
{showChecker && (
|
|
|
<div className="mb-8 bg-orange-50/50 border border-orange-200 rounded-xl p-6 animate-in slide-in-from-top-2 fade-in duration-300">
|
|
<div className="mb-8 bg-orange-50/50 border border-orange-200 rounded-xl p-6 animate-in slide-in-from-top-2 fade-in duration-300">
|
|
|
- {/* ...原有的 Check user 表单代码不变... */}
|
|
|
|
|
|
|
+ <div className="flex justify-between items-start mb-4">
|
|
|
|
|
+ <h3 className="font-bold text-orange-900 flex items-center gap-2">
|
|
|
|
|
+ <ShieldAlert size={20} /> 预订资格预检 API (Raw Response)
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <button onClick={() => setShowChecker(false)} className="text-orange-400 hover:text-orange-600">
|
|
|
|
|
+ <X size={20}/>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <form onSubmit={handleCheckUser} className="flex flex-col md:flex-row gap-4 items-end mb-4">
|
|
|
|
|
+ <div className="flex-1 w-full md:w-auto">
|
|
|
|
|
+ <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">First Name</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ required type="text" placeholder="e.g. Hongping"
|
|
|
|
|
+ className="w-full border border-orange-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-orange-400 outline-none"
|
|
|
|
|
+ value={checkForm.first_name} onChange={e => setCheckForm({...checkForm, first_name: e.target.value})}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex-1 w-full md:w-auto">
|
|
|
|
|
+ <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">Last Name</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ required type="text" placeholder="e.g. Liu"
|
|
|
|
|
+ className="w-full border border-orange-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-orange-400 outline-none"
|
|
|
|
|
+ value={checkForm.last_name} onChange={e => setCheckForm({...checkForm, last_name: e.target.value})}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex-1 w-full md:w-auto">
|
|
|
|
|
+ <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">Birthday</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ required type="date"
|
|
|
|
|
+ className="w-full border border-orange-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-orange-400 outline-none"
|
|
|
|
|
+ value={checkForm.birthday} onChange={e => setCheckForm({...checkForm, birthday: e.target.value})}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ disabled={checkLoading}
|
|
|
|
|
+ className="w-full md:w-auto px-6 py-2.5 bg-orange-600 text-white rounded-lg font-bold hover:bg-orange-700 transition disabled:opacity-70 shadow-sm flex items-center justify-center gap-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ {checkLoading ? <Loader2 size={16} className="animate-spin" /> : <Terminal size={16} />}
|
|
|
|
|
+ 调用 API
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Raw Response Viewer */}
|
|
|
|
|
+ {apiResult && (
|
|
|
|
|
+ <div className="bg-slate-900 rounded-lg overflow-hidden border border-slate-700 shadow-inner">
|
|
|
|
|
+ <div className="bg-slate-800 px-4 py-2 flex justify-between items-center text-xs text-slate-300 border-b border-slate-700">
|
|
|
|
|
+ <span className="font-mono">Response</span>
|
|
|
|
|
+ <span className={`font-bold px-2 py-0.5 rounded ${apiResult.status >= 200 && apiResult.status < 300 ? 'bg-green-900 text-green-300' : 'bg-red-900 text-red-300'}`}>
|
|
|
|
|
+ HTTP {apiResult.status}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="p-4 overflow-x-auto">
|
|
|
|
|
+ <pre className="text-xs font-mono text-green-400 whitespace-pre-wrap break-all">
|
|
|
|
|
+ {JSON.stringify(apiResult.data, null, 2)}
|
|
|
|
|
+ </pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {/* === 4. 统计看板 === */}
|
|
|
|
|
|
|
+ {/* === 3. 统计看板 === */}
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
|
|
<div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
|
|
<div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
|
|
|
<div>
|
|
<div>
|
|
@@ -225,21 +333,21 @@ export default function AdminSlotsPage() {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* === 5. Daily Slot Dashboard (主视图) === */}
|
|
|
|
|
|
|
+ {/* === 4. Daily Slot Dashboard (主视图) === */}
|
|
|
<DailySlotDashboard
|
|
<DailySlotDashboard
|
|
|
date={searchDate}
|
|
date={searchDate}
|
|
|
capacityData={slots}
|
|
capacityData={slots}
|
|
|
grabbedTasks={grabbedTasks}
|
|
grabbedTasks={grabbedTasks}
|
|
|
loading={loading}
|
|
loading={loading}
|
|
|
- onTaskClick={handleTaskClick}
|
|
|
|
|
|
|
+ onTaskClick={handleTaskClick} // 绑定点击事件,弹出操作框
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- {/* === 任务详情气泡组件 === */}
|
|
|
|
|
|
|
+ {/* === 5. 任务操作气泡组件 === */}
|
|
|
<DashboardTaskPopup
|
|
<DashboardTaskPopup
|
|
|
isOpen={isTaskModalOpen}
|
|
isOpen={isTaskModalOpen}
|
|
|
onClose={() => setIsTaskModalOpen(false)}
|
|
onClose={() => setIsTaskModalOpen(false)}
|
|
|
task={selectedTask}
|
|
task={selectedTask}
|
|
|
- anchorEl={anchorEl}
|
|
|
|
|
|
|
+ anchorEl={anchorEl} // 传入锚点
|
|
|
onSuccess={handleTaskSuccess}
|
|
onSuccess={handleTaskSuccess}
|
|
|
/>
|
|
/>
|
|
|
|
|
|