瀏覽代碼

feat: update

root 5 小時之前
父節點
當前提交
ae9da31606
共有 31 個文件被更改,包括 36 次插入3767 次删除
  1. 2 2
      .env.local
  2. 2 2
      package-lock.json
  3. 1 1
      package.json
  4. 0 148
      src/app/admin/cards/page.tsx
  5. 0 48
      src/app/admin/remote-server/page.tsx
  6. 0 356
      src/app/admin/slots/page.tsx
  7. 1 1
      src/app/contact/page.tsx
  8. 0 142
      src/app/knowledge/page.tsx
  9. 1 1
      src/app/payment/confirm/page.tsx
  10. 4 4
      src/app/refund-policy/page.tsx
  11. 2 2
      src/components/Footer.tsx
  12. 1 7
      src/components/Navbar.tsx
  13. 2 5
      src/components/admin/AdminSidebar.tsx
  14. 0 292
      src/components/admin/cards/CardModal.tsx
  15. 0 155
      src/components/admin/cards/CardTable.tsx
  16. 0 261
      src/components/admin/remote-server/ConfigManager.tsx
  17. 0 381
      src/components/admin/remote-server/ContainerScheduleManager.tsx
  18. 0 274
      src/components/admin/remote-server/DockerControl.tsx
  19. 0 253
      src/components/admin/remote-server/LogViewer.tsx
  20. 0 362
      src/components/admin/remote-server/RemoteServerControl.tsx
  21. 0 163
      src/components/admin/slots/DailySlotDashboard.tsx
  22. 0 202
      src/components/admin/slots/DashboardTaskPopup.tsx
  23. 0 215
      src/components/admin/slots/ProbabilityManager.tsx
  24. 0 207
      src/components/admin/slots/TroovBookLimitManager.tsx
  25. 0 161
      src/components/admin/slots/TroovSessionManager.tsx
  26. 5 5
      src/components/admin/tasks/ExpiringTaskAlert.tsx
  27. 3 3
      src/components/admin/tasks/TaskTable.tsx
  28. 4 4
      src/components/dashboard/Sidebar.tsx
  29. 0 102
      src/components/knowledge/KnowledgeCard.tsx
  30. 4 4
      src/lib/i18n/locales/en.ts
  31. 4 4
      src/lib/i18n/locales/zh.ts

+ 2 - 2
.env.local

@@ -1,4 +1,4 @@
 # .env.local
 # 如果你的后端在本地运行,通常是 http://127.0.0.1:8000
-# 如果已经部署到服务器,填写服务器地址,如 https://api.visafly.com
-NEXT_PUBLIC_API_URL=https://visafly.top
+# 如果已经部署到服务器,填写服务器地址,如 https://api.text.skin.com
+NEXT_PUBLIC_API_URL=https://api.text.skin

+ 2 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
-    "name": "visafly-frontend",
+    "name": "textskin-frontend",
     "version": "0.1.0",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
-            "name": "visafly-frontend",
+            "name": "textskin-frontend",
             "version": "0.1.0",
             "dependencies": {
                 "axios": "^1.6.7",

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
-    "name": "visafly-frontend",
+    "name": "textskin-frontend",
     "version": "0.1.0",
     "private": true,
     "scripts": {

+ 0 - 148
src/app/admin/cards/page.tsx

@@ -1,148 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { Plus, RefreshCw, Search } from 'lucide-react';
-import CardTable from '@/components/admin/cards/CardTable';
-import CardModal from '@/components/admin/cards/CardModal';
-import Pagination from '@/components/common/Pagination';
-import { CardData } from '@/types/card'; 
-
-export default function AdminCardsPage() {
-  const [cards, setCards] = useState<CardData[]>([]);
-  const [loading, setLoading] = useState(true);
-  const [total, setTotal] = useState(0);
-  
-  // 筛选与分页
-  const [page, setPage] = useState(1);
-  const [pageSize] = useState(10);
-  const [keyword, setKeyword] = useState('');
-  
-  // 弹窗状态
-  const [isModalOpen, setIsModalOpen] = useState(false);
-  const [selectedCard, setSelectedCard] = useState<CardData | null>(null);
-
-  useEffect(() => {
-    fetchCards(1);
-  }, []);
-
-  const fetchCards = async (targetPage: number) => {
-    setLoading(true);
-    try {
-      const res = await api.get('/api/cards/view2', {
-        params: {
-          keyword: keyword,
-          page: targetPage,
-          size: pageSize,
-          culture: 'english'
-        }
-      });
-
-      const responseBody = res.data;
-      const pageData = responseBody?.data; 
-
-      if (pageData && Array.isArray(pageData.items)) {
-        setCards(pageData.items);
-        setTotal(pageData.total || 0);
-      } else {
-        setCards([]);
-        setTotal(0);
-      }
-      
-      setPage(targetPage);
-    } catch (e) {
-      console.error("Fetch cards failed", e);
-      setCards([]);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const handleSearch = () => fetchCards(1);
-
-  const openCreate = () => {
-    setSelectedCard(null);
-    setIsModalOpen(true);
-  };
-
-  const openEdit = (card: CardData) => {
-    setSelectedCard(card);
-    setIsModalOpen(true);
-  };
-
-  return (
-    <div className="p-4 md:p-6">
-      
-      {/* === 头部工具栏:响应式布局 === */}
-      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
-        
-        {/* 标题区域 */}
-        <div>
-          <h1 className="text-2xl font-bold text-slate-800">卡片/指南管理</h1>
-          <p className="text-sm text-slate-500 mt-1">管理前端展示的签证指南和知识库内容</p>
-        </div>
-
-        {/* 操作区域:移动端垂直排列,桌面端水平排列 */}
-        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
-          
-          {/* 搜索框:移动端全宽 */}
-          <div className="relative w-full sm:w-auto flex-1 md:flex-none">
-            <input 
-              type="text" 
-              placeholder="Search title..." 
-              className="w-full md:w-64 pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
-              value={keyword}
-              onChange={e => setKeyword(e.target.value)}
-              onKeyDown={e => e.key === 'Enter' && handleSearch()}
-            />
-            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
-          </div>
-
-          {/* 按钮组:移动端横向排列,自动填充 */}
-          <div className="flex gap-2">
-            <button 
-              onClick={() => fetchCards(page)} 
-              className="p-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 transition flex-shrink-0"
-              title="刷新列表"
-            >
-              <RefreshCw size={18} />
-            </button>
-            
-            <button 
-              onClick={openCreate} 
-              className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 text-sm font-bold transition shadow-sm whitespace-nowrap"
-            >
-              <Plus size={16} /> 发布卡片
-            </button>
-          </div>
-
-        </div>
-      </div>
-
-      {/* 表格组件 (内部已适配移动端卡片视图) */}
-      <CardTable 
-        cards={cards} 
-        loading={loading} 
-        onEdit={openEdit} 
-      />
-
-      {/* 分页组件 */}
-      <div className="mt-4">
-        <Pagination 
-          currentPage={page} 
-          total={total} 
-          pageSize={pageSize} 
-          onPageChange={fetchCards} 
-        />
-      </div>
-
-      {/* 弹窗组件 */}
-      <CardModal 
-        isOpen={isModalOpen} 
-        onClose={() => setIsModalOpen(false)} 
-        onSuccess={() => fetchCards(page)}
-        card={selectedCard}
-      />
-    </div>
-  );
-}

+ 0 - 48
src/app/admin/remote-server/page.tsx

@@ -1,48 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import { Server, Clock } from 'lucide-react';
-import RemoteServerControl from '@/components/admin/remote-server/RemoteServerControl';
-// 注意这里的路径,请确保和你的实际路径一致
-import ContainerScheduleManager from '@/components/admin/remote-server/ContainerScheduleManager';
-
-export default function RemoteServerPage() {
-  // 增加顶级模块切换状态
-  const [activeModule, setActiveModule] = useState<'remote' | 'schedule'>('remote');
-
-  return (
-    <div className="space-y-6">
-      {/* 页面头部及顶级导航 */}
-      <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
-        <h1 className="text-3xl font-bold text-slate-800">
-          {activeModule === 'remote' ? '远程服务器控制' : 'Docker 运行计划'}
-        </h1>
-
-        {/* 顶级模块切换器 */}
-        <div className="flex bg-slate-200/50 p-1 rounded-lg w-full sm:w-auto">
-          <button
-            onClick={() => setActiveModule('schedule')}
-            className={`flex-1 sm:flex-none flex justify-center items-center gap-2 px-5 py-2 text-sm font-bold rounded-md transition-all ${
-              activeModule === 'schedule' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
-            }`}
-          >
-            <Clock size={16} /> 运行计划
-          </button>
-          <button
-            onClick={() => setActiveModule('remote')}
-            className={`flex-1 sm:flex-none flex justify-center items-center gap-2 px-5 py-2 text-sm font-bold rounded-md transition-all ${
-              activeModule === 'remote' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'
-            }`}
-          >
-            <Server size={16} /> 远程控制
-          </button>
-        </div>
-      </div>
-
-      {activeModule === 'schedule' && <ContainerScheduleManager />}
-      {/* 模块内容区:完全独立的两个组件 */}
-      {activeModule === 'remote' && <RemoteServerControl />}
-      
-    </div>
-  );
-}

+ 0 - 356
src/app/admin/slots/page.tsx

@@ -1,356 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { 
-  Search, Loader2, Users, Clock, Percent, 
-  AlertTriangle, CheckCircle, ShieldAlert, X, Terminal,
-  Lock, Settings2 // 引入了新功能的图标
-} from 'lucide-react';
-
-// === 引入子组件 ===
-import ProbabilityManager from '@/components/admin/slots/ProbabilityManager';
-import DailySlotDashboard from '@/components/admin/slots/DailySlotDashboard';
-import DashboardTaskPopup from '@/components/admin/slots/DashboardTaskPopup';
-import TroovSessionManager from '@/components/admin/slots/TroovSessionManager';
-import TroovBookLimitManager from '@/components/admin/slots/TroovBookLimitManager'; // 引入名额配置组件
-
-// Slot 容量数据类型
-interface SlotItem {
-  time: string;
-  rate: string | number;
-  capacity: number;
-}
-
-export default function AdminSlotsPage() {
-  const [loading, setLoading] = useState(false);
-  
-  // === 数据源状态 ===
-  const [slots, setSlots] = useState<SlotItem[]>([]); // 容量数据
-  const [grabbedTasks, setGrabbedTasks] = useState<any[]>([]); // 当天的任务数据
-  
-  // 日期选择
-  const today = new Date().toISOString().split('T')[0];
-  const [searchDate, setSearchDate] = useState(today);
-
-  // === 功能面板开关 ===
-  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 [checkForm, setCheckForm] = useState({ first_name: '', last_name: '', birthday: '' });
-  const [apiResult, setApiResult] = useState<any>(null); // 存储 API 原始响应
-
-  // === 任务详情弹窗状态 ===
-  const [selectedTask, setSelectedTask] = useState<any>(null);
-  const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
-  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
-
-  // 初始化加载 & 切换日期时加载
-  useEffect(() => {
-    fetchData();
-  }, [searchDate]);
-
-  // === 核心数据获取逻辑 ===
-  const fetchData = async () => {
-    if (!searchDate) return alert("请选择日期");
-    setLoading(true);
-    
-    try {
-      // 并行请求:容量数据 + 任务数据
-      const ratePromise = api.get('/api/troov/rate', { 
-        params: { date: searchDate } 
-      });
-      
-      const taskPromise = api.get('/api/vas/task/list', { 
-        params: { 
-          page: 1, 
-          size: 200, 
-          status: 'grabbed' 
-        } 
-      });
-
-      const [rateRes, taskRes] = await Promise.all([ratePromise, taskPromise]);
-
-      // 1. 处理 Slot 容量数据
-      const list = Array.isArray(rateRes.data.data) ? rateRes.data.data : (rateRes.data.data || []);
-      // 按时间排序
-      list.sort((a: SlotItem, b: SlotItem) => a.time.localeCompare(b.time));
-      setSlots(list);
-
-      // 2. 处理任务数据
-      const taskList = taskRes.data.data?.items || [];
-      setGrabbedTasks(taskList);
-
-    } catch (e) {
-      console.warn("API Error", e);
-      setSlots([]);
-      setGrabbedTasks([]);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  // === 用户冲突检测逻辑 ===
-  const handleCheckUser = async (e: React.FormEvent) => {
-    e.preventDefault();
-    if (!checkForm.first_name || !checkForm.last_name || !checkForm.birthday) {
-      return alert("请填写完整信息");
-    }
-
-    setCheckLoading(true);
-    setApiResult(null);
-
-    try {
-      const res = await api.post('/api/troov/book', checkForm);
-      setApiResult({
-        status: res.status,
-        statusText: res.statusText,
-        data: res.data
-      });
-    } catch (error: any) {
-      console.error(error);
-      setApiResult({
-        status: error.response?.status || 'Network Error',
-        statusText: error.response?.statusText || 'Failed',
-        data: error.response?.data || error.message
-      });
-    } finally {
-      setCheckLoading(false);
-    }
-  };
-
-  // === 任务操作处理 ===
-  const handleTaskClick = (task: any, target: HTMLElement) => {
-    setAnchorEl(target); // 保存锚点
-    setSelectedTask(task);
-    setIsTaskModalOpen(true);
-  };
-
-  const handleTaskSuccess = () => {
-    // 任务操作成功(如标记完成)后,刷新数据
-    fetchData();
-  };
-
-  // === 统计计算 ===
-  const totalCapacity = slots.reduce((acc, curr) => acc + curr.capacity, 0);
-  const validSlots = slots.filter(s => s.capacity > 0 && Number(s.rate) <= 100).length;
-  const riskSlots = slots.filter(s => Number(s.rate) > 100).length;
-
-  return (
-    <div className="p-4 md:p-6">
-      
-      {/* === 1. 顶部 Header & 操作栏 === */}
-      <div className="flex flex-col xl:flex-row justify-between items-start xl:items-center mb-6 gap-4">
-        <div>
-          <h1 className="text-2xl font-bold text-slate-800">Slot 监控与风险预警</h1>
-          <p className="text-sm text-slate-500 mt-1">综合视图:容量风险 (Rate) + 抢单结果 (Grabbed)</p>
-        </div>
-
-        {/* 顶部按钮群 */}
-        <div className="flex flex-wrap gap-3 w-full xl:w-auto">
-          
-          {/* 锁单池管理按钮 */}
-          <button 
-            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
-              ${showSessionManager ? 'bg-indigo-50 border-indigo-200 text-indigo-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
-            `}
-          >
-            <Lock size={16} />
-            {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 
-            onClick={() => setShowChecker(!showChecker)}
-            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
-              ${showChecker ? 'bg-orange-50 border-orange-200 text-orange-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
-            `}
-          >
-            <ShieldAlert size={16} />
-            {showChecker ? '关闭检测' : '冲突检测'}
-          </button>
-
-          {/* 概率管理开关 */}
-          <button 
-            onClick={() => setShowProbManager(!showProbManager)}
-            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
-              ${showProbManager ? 'bg-purple-50 border-purple-200 text-purple-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
-            `}
-          >
-            <Percent size={16} />
-            {showProbManager ? '关闭概率' : '概率管理'}
-          </button>
-
-          {/* 日期选择与刷新 */}
-          <div className="flex items-center gap-2 bg-white p-1 rounded-lg border border-slate-200 shadow-sm w-full md:w-auto">
-            <div className="relative flex-1 md:flex-none">
-              <input 
-                type="date" 
-                className="w-full pl-8 pr-2 py-1.5 text-sm border-none outline-none bg-transparent text-slate-700 font-medium"
-                value={searchDate}
-                onChange={(e) => setSearchDate(e.target.value)}
-              />
-              <Clock size={14} className="absolute left-2.5 top-2.5 text-slate-400" />
-            </div>
-            <button 
-              onClick={fetchData}
-              disabled={loading}
-              className="p-1.5 bg-slate-900 text-white rounded-md hover:bg-slate-800 transition disabled:opacity-70"
-            >
-              {loading ? <Loader2 size={16} className="animate-spin" /> : <Search size={16} />}
-            </button>
-          </div>
-        </div>
-      </div>
-
-      {/* === 2. 动态插入的组件面板 === */}
-
-      {/* 2.1 锁单池管理面板 */}
-      {showSessionManager && (
-        <div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
-          <TroovSessionManager />
-        </div>
-      )}
-
-      {/* 2.2 预约名额配置面板 */}
-      {showLimitManager && (
-        <div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
-          <TroovBookLimitManager />
-        </div>
-      )}
-
-      {/* 2.3 概率管理面板 */}
-      {showProbManager && (
-        <div className="mb-8 animate-in fade-in slide-in-from-top-4 duration-300">
-          <ProbabilityManager />
-        </div>
-      )}
-
-      {/* 2.4 用户冲突检测面板 */}
-      {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="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>
-      )}
-
-      {/* === 3. 统计看板 === */}
-      <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>
-            <p className="text-xs text-slate-400 font-bold uppercase mb-1">总释放名额</p>
-            <p className="text-2xl font-bold text-slate-800">{totalCapacity}</p>
-          </div>
-          <div className="p-3 bg-blue-50 text-blue-600 rounded-full"><Users size={20} /></div>
-        </div>
-        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
-          <div>
-            <p className="text-xs text-slate-400 font-bold uppercase mb-1">有效时间点 (Safe)</p>
-            <p className="text-2xl font-bold text-green-600">{validSlots}</p>
-          </div>
-          <div className="p-3 bg-green-50 text-green-600 rounded-full"><CheckCircle size={20} /></div>
-        </div>
-        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
-          <div>
-            <p className="text-xs text-slate-400 font-bold uppercase mb-1">高风险 (Rate &gt; 100)</p>
-            <p className="text-2xl font-bold text-red-600">{riskSlots}</p>
-          </div>
-          <div className="p-3 bg-red-50 text-red-600 rounded-full"><AlertTriangle size={20} /></div>
-        </div>
-      </div>
-
-      {/* === 4. Daily Slot Dashboard (主视图) === */}
-      <DailySlotDashboard 
-        date={searchDate}
-        capacityData={slots}
-        grabbedTasks={grabbedTasks}
-        loading={loading}
-        onTaskClick={handleTaskClick} // 绑定点击事件,弹出操作框
-      />
-
-      {/* === 5. 任务操作气泡组件 === */}
-      <DashboardTaskPopup 
-          isOpen={isTaskModalOpen}
-          onClose={() => setIsTaskModalOpen(false)}
-          task={selectedTask}
-          anchorEl={anchorEl} // 传入锚点
-          onSuccess={handleTaskSuccess}
-      />
-
-    </div>
-  );
-}

+ 1 - 1
src/app/contact/page.tsx

@@ -20,7 +20,7 @@ export default function ContactPage() {
             <Mail className="text-blue-600" />
             <div>
               <p className="text-xs text-slate-500 uppercase tracking-[0.3em]">Email</p>
-              <p className="text-sm font-semibold text-slate-900">support@visafly.top</p>
+              <p className="text-sm font-semibold text-slate-900">support@text.skin</p>
             </div>
           </div>
           <div className="flex items-start gap-3">

+ 0 - 142
src/app/knowledge/page.tsx

@@ -1,142 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { Search, BookOpen, Loader2 } from 'lucide-react';
-import KnowledgeCard from '@/components/knowledge/KnowledgeCard';
-import Pagination from '@/components/common/Pagination';
-import { useLanguage } from '@/lib/i18n/LanguageContext';
-import { CardData } from '@/types/card';
-
-export default function KnowledgePage() {
-  const [loading, setLoading] = useState(true);
-  const [cards, setCards] = useState<CardData[]>([]);
-  
-  const { t, lang } = useLanguage();
-
-  const [keyword, setKeyword] = useState('');
-  const [page, setPage] = useState(1);
-  const [pageSize] = useState(9);
-  const [total, setTotal] = useState(0);
-
-  useEffect(() => {
-    fetchCards(1);
-  }, [lang]);
-
-  const fetchCards = async (targetPage: number) => {
-    setLoading(true);
-    try {
-      const cultureParam = lang === 'zh' ? 'chinese' : 'english';
-      const res = await api.get('/api/cards/view2', {
-        params: {
-          keyword: keyword,
-          page: targetPage,
-          size: pageSize,
-          culture: cultureParam
-        }
-      });
-
-      const data = res.data.data || {};
-      
-      if (Array.isArray(data)) {
-        setCards(data);
-        setTotal(data.length);
-      } else {
-        setCards(data.items || []);
-        setTotal(data.total || 0);
-      }
-      setPage(targetPage);
-
-    } catch (error) {
-      console.warn("API Error", error);
-      setCards([]); 
-      setTotal(0);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const handleSearch = () => {
-    fetchCards(1);
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') handleSearch();
-  };
-
-  return (
-    // 调整 1: 移动端减少垂直 Padding
-    <div className="min-h-screen bg-slate-50 py-6 px-4 md:py-12 md:px-6">
-      <div className="max-w-7xl mx-auto">
-        
-        {/* Header Section */}
-        <div className="text-center mb-8 md:mb-12">
-          {/* 调整 2: 响应式字体大小 */}
-          <h1 className="text-2xl md:text-3xl font-bold text-slate-900 flex items-center justify-center gap-2 md:gap-3">
-            <BookOpen className="text-blue-600 w-6 h-6 md:w-8 md:h-8" /> 
-            {t('knowledge.title')}
-          </h1>
-          <p className="text-sm md:text-base text-slate-500 mt-2 md:mt-3 max-w-2xl mx-auto px-2">
-            {t('knowledge.subtitle')}
-          </p>
-        </div>
-
-        {/* Search Bar */}
-        <div className="max-w-2xl mx-auto mb-8 md:mb-12 relative">
-          <div className="relative group">
-            <input 
-              type="text" 
-              placeholder={t('knowledge.search_placeholder')} 
-              // 调整 3: 移动端输入框稍微紧凑一点,调整 padding 防止文字被按钮遮挡
-              className="w-full pl-10 md:pl-12 pr-24 md:pr-28 py-3 md:py-4 rounded-xl border border-slate-200 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-base md:text-lg transition"
-              value={keyword}
-              onChange={(e) => setKeyword(e.target.value)}
-              onKeyDown={handleKeyDown}
-            />
-            
-            {/* 图标尺寸适配 */}
-            <Search className="absolute left-3 md:left-4 top-1/2 -translate-y-1/2 text-slate-400 w-5 h-5 md:w-6 md:h-6" />
-            
-            {/* 按钮尺寸适配 */}
-            <button 
-              onClick={handleSearch}
-              className="absolute right-1.5 md:right-2 top-1/2 -translate-y-1/2 bg-slate-900 text-white px-4 md:px-6 py-1.5 md:py-2.5 rounded-lg font-bold hover:bg-slate-800 transition text-sm md:text-base shadow-sm active:scale-95"
-            >
-              {t('common.search')}
-            </button>
-          </div>
-        </div>
-
-        {/* Content Grid */}
-        {loading ? (
-          <div className="flex justify-center py-20">
-            <Loader2 className="animate-spin text-blue-600 w-8 h-8 md:w-10 md:h-10" />
-          </div>
-        ) : cards.length === 0 ? (
-          <div className="text-center py-16 md:py-20 text-slate-400">
-            <BookOpen size={48} className="mx-auto mb-4 opacity-20 w-12 h-12 md:w-16 md:h-16" />
-            <p>{t('knowledge.empty_state')}</p>
-          </div>
-        ) : (
-          // Grid 布局在移动端默认 cols-1,这里保持不变即可
-          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 items-start">
-            {cards.map((card) => (
-              <KnowledgeCard key={card.id} data={card} />
-            ))}
-          </div>
-        )}
-
-        {/* Pagination */}
-        <div className="mt-6 md:mt-8">
-          <Pagination 
-            currentPage={page}
-            total={total}
-            pageSize={pageSize}
-            onPageChange={(p) => fetchCards(p)}
-          />
-        </div>
-
-      </div>
-    </div>
-  );
-}

+ 1 - 1
src/app/payment/confirm/page.tsx

@@ -142,7 +142,7 @@ export default function PaymentConfirmPage() {
       </Suspense>
 
       <p className="mt-8 text-xs text-slate-400">
-        Visafly Admin System &copy; {new Date().getFullYear()}
+        TextSkin Admin System &copy; {new Date().getFullYear()}
       </p>
     </div>
   );

+ 4 - 4
src/app/refund-policy/page.tsx

@@ -19,9 +19,9 @@ const WhatsAppIcon = ({ className }) => (
 export default function RefundPolicyPage() {
   const { t } = useLanguage();
 
-  const TELEGRAM_USERNAME = "Visafly Support"
+  const TELEGRAM_USERNAME = "TextSkin Support"
   const TELEGRAM_NUMBER = "+8617386033451"; 
-  const WHATSAPP_NUMBER = "353892125284";  
+  const WHATSAPP_NUMBER = "8617386033451";  
 
   return (
     <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
@@ -157,7 +157,7 @@ export default function RefundPolicyPage() {
             
             <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
               <a 
-                href="mailto:support@visafly.top" 
+                href="mailto:support@text.skin" 
                 className="flex items-center gap-3 p-4 rounded-xl border border-slate-200 bg-white hover:border-blue-500 hover:bg-blue-50 transition-all group"
               >
                 <div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
@@ -165,7 +165,7 @@ export default function RefundPolicyPage() {
                 </div>
                 <div className="flex-1 min-w-0">
                   <div className="text-sm font-semibold text-slate-900">Email Support</div>
-                  <div className="text-xs text-slate-500 truncate">support@visafly.top</div>
+                  <div className="text-xs text-slate-500 truncate">support@test.skin</div>
                 </div>
               </a>
 

+ 2 - 2
src/components/Footer.tsx

@@ -30,7 +30,7 @@ export default function Footer() {
           <div className="col-span-2 text-center md:text-left">
             <div className="flex items-center justify-center md:justify-start gap-2 mb-4">
               <Plane className="text-blue-600" size={24} />
-              <span className="text-xl font-bold text-slate-900">Visafly</span>
+              <span className="text-xl font-bold text-slate-900">TextSkin</span>
             </div>
             <p className="text-sm text-slate-500 max-w-xs mx-auto md:mx-0 leading-relaxed">
               {t('footer.description')}
@@ -61,7 +61,7 @@ export default function Footer() {
         {/* Bottom Bar - 移动端垂直排列,桌面端水平两端对齐 */}
         <div className="border-t border-slate-100 pt-8 flex flex-col md:flex-row justify-between items-center text-xs text-slate-400 gap-4 md:gap-0">
           <p className="text-center md:text-left">
-            &copy; {currentYear} Visafly Inc. {t('footer.rights_reserved')}
+            &copy; {currentYear} TextSkin Inc. {t('footer.rights_reserved')}
           </p>
           <div className="flex gap-6 md:gap-4">
             <Link href="/privacy" className="hover:text-slate-600 transition p-2 md:p-0">{t('footer.privacy')}</Link>

+ 1 - 7
src/components/Navbar.tsx

@@ -54,7 +54,7 @@ export default function Navbar() {
           {/* Logo */}
           <div className="flex items-center cursor-pointer" onClick={() => router.push('/')}>
             <Plane className="text-blue-600 mr-2" />
-            <span className="text-2xl font-bold text-blue-600">Visafly</span>
+            <span className="text-2xl font-bold text-blue-600">TextSkin</span>
           </div>
 
           {/* Desktop Nav */}
@@ -65,9 +65,6 @@ export default function Navbar() {
             <Link href="/slots" className="nav-link flex items-center gap-1">
               <CalendarSearch size={18} /> {t('nav.slots') || '名额查询'}
             </Link>
-            <Link href="/knowledge" className="nav-link flex items-center gap-1">
-              <BookOpen size={18} /> {t('nav.guide') || '办理指南'}
-            </Link>
           </div>
 
           {/* Desktop Actions */}
@@ -130,9 +127,6 @@ export default function Navbar() {
                 <MobileLink href="/slots" icon={CalendarSearch} onClick={handleLinkClick}>
                   {t('nav.slots') || '名额查询'}
                 </MobileLink>
-                <MobileLink href="/knowledge" icon={BookOpen} onClick={handleLinkClick}>
-                  {t('nav.guide') || '办理指南'}
-                </MobileLink>
               </div>
               <div className="pt-2 space-y-2">
                 {isLogged ? (

+ 2 - 5
src/components/admin/AdminSidebar.tsx

@@ -32,10 +32,7 @@ export default function AdminSidebar() {
     { name: '支付配置', href: '/admin/payments', icon: CreditCard },
     { name: '商品配置', href: '/admin/products', icon: Settings },
     { name: '系统任务', href: '/admin/tasks', icon: Activity },
-    { name: 'TROOV', href: '/admin/slots', icon: CalendarClock },
     { name: 'VISAMETRIC', href: '/admin/visametric', icon: CalendarClock },
-    { name: '卡片管理', href: '/admin/cards', icon: LayoutGrid },
-    { name: '服务器管理', href: '/admin/remote-server', icon: Server },
     { name: 'Worker 状态', href: '/admin/workers', icon: Server },
     { name: '账号池管理', href: '/admin/accounts', icon: UserCog },
     { name: '代理管理', href: '/admin/proxies', icon: Network },
@@ -73,7 +70,7 @@ export default function AdminSidebar() {
       <div className="md:hidden fixed top-0 left-0 right-0 h-16 bg-slate-900 border-b border-slate-800 flex items-center justify-between px-4 z-40 shadow-md">
         <div className="flex items-center gap-2 font-bold text-xl text-white">
           <Plane className="text-blue-500" size={24} />
-          <span>Visafly Admin</span>
+          <span>TextSkin Admin</span>
         </div>
         <button 
           onClick={() => setIsMobileOpen(!isMobileOpen)}
@@ -110,7 +107,7 @@ export default function AdminSidebar() {
         <div className={`h-16 items-center border-b border-slate-800 transition-all duration-300 hidden md:flex flex-shrink-0 ${isCollapsed ? 'justify-center px-0' : 'px-6 gap-3'}`}>
           <Plane className="text-blue-500 flex-shrink-0" size={24} />
           <div className={`font-bold text-xl whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
-            <span className="text-blue-400">Visafly</span> Admin
+            <span className="text-blue-400">TextSkin</span> Admin
           </div>
         </div>
 

+ 0 - 292
src/components/admin/cards/CardModal.tsx

@@ -1,292 +0,0 @@
-'use client';
-
-import { useState, useEffect, useRef } from 'react';
-import api from '@/lib/api';
-import { X, Loader2, Upload, Image as ImageIcon } from 'lucide-react';
-import { CardData } from '@/types/card';
-
-interface CardModalProps {
-  isOpen: boolean;
-  onClose: () => void;
-  onSuccess: () => void;
-  card?: CardData | null; // 如果有值则是编辑模式
-}
-
-// 辅助函数:根据 image 字段(可能是 ID 或 URL)生成预览链接
-const getPreviewUrl = (imageField: string | null) => {
-  if (!imageField) return '';
-  if (imageField.startsWith('http') || imageField.startsWith('data:')) {
-    return imageField;
-  }
-  return `/api/resource/download_file?fid=${encodeURIComponent(imageField)}`;
-};
-
-export default function CardModal({ isOpen, onClose, onSuccess, card }: CardModalProps) {
-  const [loading, setLoading] = useState(false);
-  const fileInputRef = useRef<HTMLInputElement>(null);
-  
-  // 表单状态
-  const [form, setForm] = useState<CardData>({
-    id: 0,
-    title: '',
-    content: '',
-    image: '',
-    label: '',
-    country: '',
-    additional_info: '',
-    culture: 'english'
-  });
-
-  // 图片预览状态
-  const [previewUrl, setPreviewUrl] = useState('');
-
-  // 初始化:打开弹窗时填充数据
-  useEffect(() => {
-    if (isOpen) {
-      if (card) {
-        setForm(card);
-        setPreviewUrl(getPreviewUrl(card.image));
-      } else {
-        // 重置为默认值
-        setForm({
-          id: 0,
-          title: '',
-          content: '',
-          image: '',
-          label: '',
-          country: '',
-          additional_info: '',
-          culture: 'english'
-        });
-        setPreviewUrl('');
-      }
-    }
-  }, [isOpen, card]);
-
-  // 处理文件选择与上传
-  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
-    const file = e.target.files?.[0];
-    if (!file) return;
-
-    // 1. 简单校验大小 (2MB)
-    if (file.size > 2 * 1024 * 1024) {
-      alert("图片大小不能超过 2MB");
-      return;
-    }
-
-    // 2. 本地预览 (提升体验,不用等上传完就能看到)
-    setPreviewUrl(URL.createObjectURL(file));
-
-    // 3. 上传到服务器
-    const formData = new FormData();
-    formData.append('file', file);
-
-    try {
-      const res = await api.post('/api/resource/upload_file', formData, {
-        headers: { 'Content-Type': 'multipart/form-data' }
-      });
-      
-      const result = res.data.data;
-      const uploadedValue = (typeof result === 'object' && result !== null)
-        ? (result.fid || result.url)
-        : result;
-
-      setForm(prev => ({ ...prev, image: uploadedValue }));
-
-      if (typeof result === 'object' && result.url) {
-        setPreviewUrl(result.url);
-      }
-    } catch (error) {
-      console.error("Upload failed", error);
-      alert("图片上传失败,请检查网络或重试");
-    }
-  };
-
-  const handleSubmit = async (e: React.FormEvent) => {
-    e.preventDefault();
-    setLoading(true);
-    try {
-      // API: POST /api/cards/publish
-      await api.post('/api/cards/publish', form);
-      
-      alert(card ? '更新成功' : '发布成功');
-      onSuccess(); // 刷新列表
-      onClose();   // 关闭弹窗
-    } catch (error: any) {
-      console.error(error);
-      const msg = error.response?.data?.message || "操作失败";
-      alert(`错误: ${msg}`);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  if (!isOpen) return null;
-
-  const labelClass = "block text-sm font-bold text-slate-700 mb-1";
-  const inputClass = "w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition";
-
-  return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
-      <div className="bg-white rounded-xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col overflow-hidden animate-in zoom-in duration-200">
-        
-        {/* Header */}
-        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
-          <h3 className="font-bold text-gray-900 text-lg">
-            {card ? '编辑卡片' : '发布新卡片'}
-          </h3>
-          <button onClick={onClose} className="p-1 hover:bg-gray-200 rounded-full transition">
-            <X className="text-gray-400 hover:text-gray-600" size={24} />
-          </button>
-        </div>
-
-        {/* Body */}
-        <div className="p-6 overflow-y-auto">
-          <form onSubmit={handleSubmit} className="space-y-5">
-            
-            {/* 第一行:标题 & 语言 */}
-            <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
-              <div className="md:col-span-2">
-                <label className={labelClass}>标题 (Title) <span className="text-red-500">*</span></label>
-                <input 
-                  required 
-                  type="text" 
-                  className={inputClass} 
-                  value={form.title} 
-                  onChange={e => setForm({...form, title: e.target.value})} 
-                  placeholder="请输入卡片标题"
-                />
-              </div>
-              <div>
-                <label className={labelClass}>语言 (Culture)</label>
-                <select 
-                  className={inputClass} 
-                  value={form.culture} 
-                  onChange={e => setForm({...form, culture: e.target.value as any})}
-                >
-                  <option value="english">English</option>
-                  <option value="chinese">Chinese</option>
-                </select>
-              </div>
-            </div>
-
-            {/* 第二行:国家 & 标签 */}
-            <div className="grid grid-cols-2 gap-4">
-              <div>
-                <label className={labelClass}>国家 (Country)</label>
-                <input 
-                  type="text" 
-                  className={inputClass} 
-                  placeholder="e.g. France" 
-                  value={form.country} 
-                  onChange={e => setForm({...form, country: e.target.value})} 
-                />
-              </div>
-              <div>
-                <label className={labelClass}>标签 (Label)</label>
-                <input 
-                  type="text" 
-                  className={inputClass} 
-                  placeholder="e.g. Visa Guide" 
-                  value={form.label} 
-                  onChange={e => setForm({...form, label: e.target.value})} 
-                />
-              </div>
-            </div>
-
-            {/* 图片上传区域 */}
-            <div>
-              <label className={labelClass}>封面图片 (Image)</label>
-              <div className="flex items-start gap-4">
-                {/* 上传按钮/预览区 */}
-                <div 
-                  onClick={() => fileInputRef.current?.click()}
-                  className="w-32 h-24 border-2 border-dashed border-slate-300 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:bg-slate-50 hover:border-blue-400 transition bg-slate-50 flex-shrink-0 overflow-hidden relative"
-                >
-                  {previewUrl ? (
-                    <img src={previewUrl} alt="Preview" className="w-full h-full object-cover" />
-                  ) : (
-                    <>
-                      <Upload className="text-slate-400 mb-1" size={20} />
-                      <span className="text-xs text-slate-500">点击上传</span>
-                    </>
-                  )}
-                </div>
-
-                {/* 手动输入/提示区 */}
-                <div className="flex-1 space-y-2">
-                  <input 
-                    type="text" 
-                    className={inputClass} 
-                    placeholder="或者直接输入图片 URL / File ID" 
-                    value={form.image}
-                    onChange={e => {
-                      setForm({...form, image: e.target.value});
-                      // 如果用户手动输入 URL,尝试预览
-                      if(e.target.value.startsWith('http')) {
-                        setPreviewUrl(e.target.value);
-                      }
-                    }}
-                  />
-                  <p className="text-xs text-gray-400">
-                    推荐尺寸: 800x600 或 16:9 比例。支持上传本地图片或粘贴网络图片链接。
-                  </p>
-                  <input 
-                    type="file" 
-                    ref={fileInputRef} 
-                    className="hidden" 
-                    accept="image/*" 
-                    onChange={handleFileChange} 
-                  />
-                </div>
-              </div>
-            </div>
-
-            {/* 内容编辑器 (Textarea) */}
-            <div>
-              <label className={labelClass}>内容详情 (Content - 支持 HTML)</label>
-              <textarea 
-                className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none h-48 font-mono leading-relaxed resize-y"
-                placeholder="<p>在这里输入文章内容...</p>"
-                value={form.content}
-                onChange={e => setForm({...form, content: e.target.value})}
-              />
-            </div>
-
-            {/* 附加信息 */}
-            <div>
-              <label className={labelClass}>附加信息 (Additional Info - 可选)</label>
-              <input 
-                type="text" 
-                className={inputClass} 
-                value={form.additional_info} 
-                onChange={e => setForm({...form, additional_info: e.target.value})} 
-                placeholder="例如:备注、内部ID等"
-              />
-            </div>
-
-            {/* Footer Buttons */}
-            <div className="pt-4 flex justify-end gap-3 border-t mt-6">
-              <button 
-                type="button" 
-                onClick={onClose} 
-                className="px-4 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium text-slate-700 transition"
-              >
-                取消
-              </button>
-              <button 
-                type="submit" 
-                disabled={loading} 
-                className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold flex items-center gap-2 disabled:opacity-50 transition shadow-sm"
-              >
-                {loading && <Loader2 size={16} className="animate-spin" />}
-                {card ? '保存修改' : '确认发布'}
-              </button>
-            </div>
-
-          </form>
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 155
src/components/admin/cards/CardTable.tsx

@@ -1,155 +0,0 @@
-'use client';
-
-import { Edit, Image as ImageIcon, MapPin, Tag, Globe } from 'lucide-react';
-import { CardData } from '@/types/card';
-
-
-interface CardTableProps {
-  cards: CardData[];
-  loading: boolean;
-  onEdit: (card: CardData) => void;
-}
-
-const getImageUrl = (fidString: string | null) => {
-  if (!fidString) return null;
-  if (fidString.startsWith('http')) return fidString;
-  return `/api/resource/download_file?fid=${encodeURIComponent(fidString)}`;
-};
-
-export default function CardTable({ cards, loading, onEdit }: CardTableProps) {
-  if (loading) return <div className="p-12 text-center text-gray-500">加载数据中...</div>;
-  if (cards.length === 0) return <div className="p-12 text-center text-gray-500">暂无卡片数据</div>;
-
-  return (
-    <div className="space-y-4">
-      
-      {/* =========================== */}
-      {/* 1. Desktop View (Table) - 中大屏幕显示 */}
-      {/* =========================== */}
-      <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
-        <table className="min-w-full divide-y divide-slate-200">
-          <thead className="bg-slate-50">
-            <tr>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Image</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Title / Content</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Info</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Language</th>
-              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Action</th>
-            </tr>
-          </thead>
-          <tbody className="divide-y divide-slate-100">
-            {cards.map((card, idx) => {
-              const imgUrl = getImageUrl(card.image);
-              return (
-                <tr key={card.id || idx} className="hover:bg-slate-50 transition-colors">
-                  <td className="px-6 py-4">
-                    <div className="w-16 h-12 bg-slate-100 rounded overflow-hidden flex items-center justify-center border border-slate-200">
-                      {imgUrl ? (
-                        <img src={imgUrl} alt="cover" className="w-full h-full object-cover" />
-                      ) : (
-                        <ImageIcon className="text-slate-300" size={20} />
-                      )}
-                    </div>
-                  </td>
-                  <td className="px-6 py-4">
-                    <div className="text-sm font-bold text-gray-900 line-clamp-1 max-w-[200px]" title={card.title}>{card.title}</div>
-                    <div className="text-xs text-gray-500 line-clamp-1 mt-1 max-w-[200px]">{(card.content || '').replace(/<[^>]+>/g, '')}</div>
-                  </td>
-                  <td className="px-6 py-4">
-                    <div className="flex flex-col gap-1 items-start">
-                      <span className="text-xs font-medium bg-blue-50 text-blue-700 px-2 py-0.5 rounded w-fit whitespace-nowrap">
-                        {card.country || 'No Country'}
-                      </span>
-                      <span className="text-xs text-gray-500">
-                        {card.label || '-'}
-                      </span>
-                    </div>
-                  </td>
-                  <td className="px-6 py-4">
-                    <span className={`text-xs px-2 py-1 rounded font-medium ${card.culture === 'english' ? 'bg-purple-50 text-purple-700' : 'bg-red-50 text-red-700'}`}>
-                      {card.culture}
-                    </span>
-                  </td>
-                  <td className="px-6 py-4 text-right">
-                    <button 
-                      onClick={() => onEdit(card)}
-                      className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm font-medium"
-                    >
-                      <Edit size={14} className="mr-1" /> Edit
-                    </button>
-                  </td>
-                </tr>
-              );
-            })}
-          </tbody>
-        </table>
-      </div>
-
-      {/* =========================== */}
-      {/* 2. Mobile View (Cards) - 小屏幕显示 */}
-      {/* =========================== */}
-      <div className="md:hidden space-y-4">
-        {cards.map((card, idx) => {
-          const imgUrl = getImageUrl(card.image);
-          const cleanContent = (card.content || '').replace(/<[^>]+>/g, '');
-
-          return (
-            <div key={card.id || idx} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
-              
-              {/* Header: Image & Title */}
-              <div className="flex gap-4 mb-3">
-                <div className="w-20 h-20 bg-slate-100 rounded-lg overflow-hidden flex-shrink-0 border border-slate-200 flex items-center justify-center">
-                  {imgUrl ? (
-                    <img src={imgUrl} alt="cover" className="w-full h-full object-cover" />
-                  ) : (
-                    <ImageIcon className="text-slate-300" size={24} />
-                  )}
-                </div>
-                <div className="flex-1 min-w-0">
-                  <div className="flex justify-between items-start">
-                    <h3 className="text-sm font-bold text-slate-900 line-clamp-2 leading-snug mb-1">
-                      {card.title}
-                    </h3>
-                  </div>
-                  <p className="text-xs text-slate-500 line-clamp-2">
-                    {cleanContent || '暂无内容摘要'}
-                  </p>
-                </div>
-              </div>
-
-              {/* Meta Info Grid */}
-              <div className="flex flex-wrap gap-2 mb-4">
-                <span className="inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded border border-blue-100">
-                  <MapPin size={10} /> {card.country || 'Global'}
-                </span>
-                
-                {card.label && (
-                  <span className="inline-flex items-center gap-1 text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded border border-slate-200">
-                    <Tag size={10} /> {card.label}
-                  </span>
-                )}
-
-                <span className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded border ${
-                  card.culture === 'english' 
-                    ? 'bg-purple-50 text-purple-700 border-purple-100' 
-                    : 'bg-red-50 text-red-700 border-red-100'
-                }`}>
-                  <Globe size={10} /> {card.culture}
-                </span>
-              </div>
-
-              {/* Action Buttons */}
-              <button 
-                onClick={() => onEdit(card)}
-                className="w-full flex items-center justify-center gap-2 py-2.5 bg-slate-50 text-slate-700 rounded-lg text-sm font-medium border border-slate-200 active:scale-95 transition-transform hover:bg-slate-100"
-              >
-                <Edit size={16} /> 编辑卡片
-              </button>
-            </div>
-          );
-        })}
-      </div>
-
-    </div>
-  );
-}

+ 0 - 261
src/components/admin/remote-server/ConfigManager.tsx

@@ -1,261 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { 
-  Settings, Save, RefreshCw, AlertCircle, 
-  FileText, CheckCircle2, Maximize2, Minimize2 
-} from 'lucide-react';
-
-interface ServerConfig {
-  host: string;
-  port: number;
-  username: string;
-  password?: string;
-  key_file?: string;
-  project_path: string;
-}
-
-export default function ConfigManager({ 
-  serverConfig,
-  serverId,
-  projectPath
-}: { 
-  serverConfig: ServerConfig;
-  serverId?: string;
-  projectPath?: string;
-}) {
-  const [configFile, setConfigFile] = useState<string>('config/troov_config.json');
-  const [configData, setConfigData] = useState<any>(null);
-  const [configJson, setConfigJson] = useState<string>('');
-  
-  const [loading, setLoading] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-  const [success, setSuccess] = useState<string | null>(null);
-  
-  const [isValidJson, setIsValidJson] = useState(true);
-  const [jsonErrorMsg, setJsonErrorMsg] = useState('');
-
-  // 新增:全屏控制状态
-  const [isFullScreen, setIsFullScreen] = useState(false);
-
-  // 校验 JSON
-  useEffect(() => {
-    if (!configJson) {
-      setIsValidJson(true);
-      setJsonErrorMsg('');
-      return;
-    }
-    try {
-      JSON.parse(configJson);
-      setIsValidJson(true);
-      setJsonErrorMsg('');
-    } catch (e: any) {
-      setIsValidJson(false);
-      const msg = e.message.split('\n')[0];
-      setJsonErrorMsg(msg);
-    }
-  }, [configJson]);
-
-  const fetchConfig = async () => {
-    if (!configFile) {
-      setError('请先输入配置文件路径');
-      return;
-    }
-    setLoading(true);
-    setError(null);
-    setSuccess(null);
-    try {
-      const url = serverId ? '/api/remote/server/config/read' : '/api/remote/config/read';
-      const payload = serverId 
-        ? { server_id: serverId, config_file: configFile, project_path: projectPath }
-        : { ...serverConfig, config_file: configFile, project_path: projectPath };
-
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        setConfigData(response.data.data.config);
-        setConfigJson(JSON.stringify(response.data.data.config, null, 2));
-      } else {
-        setError(response.data.message || '读取配置文件失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '读取配置文件失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const updateConfigByJson = async () => {
-    if (!configFile || !configJson) {
-      setError('请填写配置文件路径和JSON内容');
-      return;
-    }
-    if (!isValidJson) {
-      setError('JSON 格式错误,请修正后再保存');
-      return;
-    }
-    setLoading(true);
-    setError(null);
-    setSuccess(null);
-    try {
-      const parsed = JSON.parse(configJson);
-      const updatePromises: Promise<any>[] = [];
-      const queue: Array<{ obj: any; prefix: string }> = [{ obj: parsed, prefix: '' }];
-      
-      while (queue.length > 0) {
-        const { obj, prefix } = queue.shift()!;
-        for (const [key, value] of Object.entries(obj)) {
-          const fullPath = prefix ? `${prefix}.${key}` : key;
-          if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
-             const url = serverId ? '/api/remote/server/config/update' : '/api/remote/config/update';
-             const payload = serverId 
-              ? { server_id: serverId, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath }
-              : { ...serverConfig, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath };
-             updatePromises.push(api.post(url, payload));
-          } else {
-            const url = serverId ? '/api/remote/server/config/update' : '/api/remote/config/update';
-            const payload = serverId 
-              ? { server_id: serverId, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath }
-              : { ...serverConfig, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath };
-            updatePromises.push(api.post(url, payload));
-          }
-        }
-      }
-      await Promise.all(updatePromises);
-      setSuccess('配置文件已全部更新');
-      setTimeout(fetchConfig, 500); 
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '更新配置文件失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  return (
-    <div className="space-y-4 h-full flex flex-col">
-      <div className="flex items-center justify-between flex-shrink-0">
-        <h3 className="text-lg font-semibold text-slate-800">配置文件管理</h3>
-      </div>
-
-      {error && (
-        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded-md flex items-center gap-2 text-sm flex-shrink-0">
-          <AlertCircle size={16} />
-          <span className="truncate">{error}</span>
-        </div>
-      )}
-
-      {success && (
-        <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-2 rounded-md flex items-center gap-2 text-sm flex-shrink-0">
-          <Settings size={16} />
-          {success}
-        </div>
-      )}
-
-      {/* 读取配置栏 */}
-      <div className="bg-white rounded-lg border border-slate-200 p-3 sm:p-4 flex-shrink-0">
-        <div className="flex flex-col sm:flex-row gap-2 items-center">
-          <div className="flex items-center gap-2 w-full sm:w-auto text-slate-600">
-             <FileText size={18} />
-             <span className="text-sm font-medium whitespace-nowrap">路径:</span>
-          </div>
-          <input
-            type="text"
-            value={configFile}
-            onChange={(e) => setConfigFile(e.target.value)}
-            placeholder="如: config/troov_config.json"
-            className="flex-1 w-full px-3 py-1.5 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm font-mono"
-          />
-          <button
-            onClick={fetchConfig}
-            disabled={loading}
-            className="w-full sm:w-auto px-5 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2 text-sm font-medium"
-          >
-            <RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
-            读取
-          </button>
-        </div>
-      </div>
-
-      {/* 核心编辑区域 */}
-      {configData && (
-        <div 
-          className={`
-            flex flex-col bg-slate-900 border border-slate-700 shadow-inner overflow-hidden transition-all duration-300
-            ${isFullScreen 
-              ? 'fixed inset-0 z-50 h-screen w-screen rounded-none' // 全屏样式
-              : 'rounded-lg min-h-[500px] h-[60vh]' // 默认样式: 增加最小高度
-            }
-          `}
-        >
-          {/* Editor Header */}
-          <div className="px-4 py-2 bg-slate-950 border-b border-slate-800 flex justify-between items-center flex-shrink-0">
-            <span className="text-xs font-mono text-slate-400 truncate max-w-[200px]">
-              {configFile}
-            </span>
-            <div className="flex items-center gap-3">
-              {/* 状态指示 */}
-              {isValidJson ? (
-                <span className="text-[10px] text-green-500 flex items-center gap-1 bg-green-900/30 px-2 py-0.5 rounded">
-                  <CheckCircle2 size={10} /> 有效
-                </span>
-              ) : (
-                <span className="text-[10px] text-red-400 flex items-center gap-1 bg-red-900/30 px-2 py-0.5 rounded" title={jsonErrorMsg}>
-                  <AlertCircle size={10} /> 错误
-                </span>
-              )}
-              
-              {/* 全屏切换按钮 */}
-              <button 
-                onClick={() => setIsFullScreen(!isFullScreen)}
-                className="text-slate-400 hover:text-white transition-colors p-1"
-                title={isFullScreen ? "退出全屏" : "全屏编辑"}
-              >
-                {isFullScreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
-              </button>
-            </div>
-          </div>
-
-          {/* Editor Body */}
-          <div className="flex-1 relative">
-            <textarea
-              value={configJson}
-              onChange={(e) => setConfigJson(e.target.value)}
-              className={`
-                w-full h-full p-4 bg-slate-900 font-mono text-xs leading-relaxed resize-none focus:outline-none
-                ${isValidJson ? 'text-green-400' : 'text-yellow-200'}
-                custom-scrollbar
-              `}
-              spellCheck={false}
-              placeholder="// 在此编辑 JSON 配置..."
-            />
-          </div>
-
-          {/* Editor Footer */}
-          <div className="p-3 bg-slate-800 border-t border-slate-700 flex justify-end flex-shrink-0 gap-3">
-            {isFullScreen && (
-              <button
-                onClick={() => setIsFullScreen(false)}
-                className="px-4 py-2 text-slate-400 hover:text-white text-sm transition"
-              >
-                取消/返回
-              </button>
-            )}
-            <button
-              onClick={updateConfigByJson}
-              disabled={loading || !isValidJson}
-              className={`
-                px-6 py-2 rounded-md flex items-center gap-2 text-sm font-bold transition-all
-                ${isValidJson 
-                  ? 'bg-blue-600 text-white hover:bg-blue-500 shadow-lg shadow-blue-900/50' 
-                  : 'bg-slate-700 text-slate-500 cursor-not-allowed'}
-              `}
-            >
-              {loading ? <RefreshCw size={16} className="animate-spin"/> : <Save size={16} />}
-              保存全部更改
-            </button>
-          </div>
-        </div>
-      )}
-    </div>
-  );
-}

+ 0 - 381
src/components/admin/remote-server/ContainerScheduleManager.tsx

@@ -1,381 +0,0 @@
-'use client';
-
-import { useState, useEffect, useRef } from 'react';
-import api from '@/lib/api';
-import { Clock, Plus, Trash2, Save, RefreshCw, AlertCircle, Globe, Info } from 'lucide-react';
-
-// ==========================================
-// 1. 紧凑版 24 小时圆环滑块 (TimeRangeDial)
-// ==========================================
-interface TimeDialProps {
-  startTime: string;
-  endTime: string;
-  onChangeStart: (time: string) => void;
-  onChangeEnd: (time: string) => void;
-}
-
-function TimeRangeDial({ startTime, endTime, onChangeStart, onChangeEnd }: TimeDialProps) {
-  const svgRef = useRef<SVGSVGElement>(null);
-  const [dragging, setDragging] = useState<'start' | 'end' | null>(null);
-
-  const timeToAngle = (timeStr: string) => {
-    if (!timeStr) return 0;
-    const [h, m] = timeStr.split(':').map(Number);
-    return ((h + m / 60) / 24) * 360;
-  };
-
-  const angleToTime = (deg: number) => {
-    let hoursFloat = (deg / 360) * 24;
-    const snap = 0.25; // 15分钟吸附
-    hoursFloat = Math.round(hoursFloat / snap) * snap;
-    if (hoursFloat >= 24) hoursFloat -= 24;
-    const h = Math.floor(hoursFloat);
-    const m = Math.round((hoursFloat - h) * 60);
-    return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`;
-  };
-
-  const polarToCartesian = (cx: number, cy: number, r: number, angleDeg: number) => {
-    const angleRad = (angleDeg - 90) * Math.PI / 180.0;
-    return { x: cx + r * Math.cos(angleRad), y: cy + r * Math.sin(angleRad) };
-  };
-
-  const getArcPath = (cx: number, cy: number, r: number, startAngle: number, endAngle: number) => {
-    if (startAngle === endAngle) return '';
-    const start = polarToCartesian(cx, cy, r, startAngle);
-    const end = polarToCartesian(cx, cy, r, endAngle);
-    let diff = endAngle - startAngle;
-    if (diff < 0) diff += 360;
-    const largeArcFlag = diff > 180 ? 1 : 0;
-    return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 1 ${end.x} ${end.y}`;
-  };
-
-  // 缩小表盘尺寸以节省空间
-  const size = 130;
-  const cx = size / 2, cy = size / 2, r = 48; 
-  const startAngle = timeToAngle(startTime);
-  const endAngle = timeToAngle(endTime);
-  const startPos = polarToCartesian(cx, cy, r, startAngle);
-  const endPos = polarToCartesian(cx, cy, r, endAngle);
-
-  useEffect(() => {
-    const handleMove = (e: PointerEvent) => {
-      if (!dragging || !svgRef.current) return;
-      const rect = svgRef.current.getBoundingClientRect();
-      const centerX = rect.left + rect.width / 2;
-      const centerY = rect.top + rect.height / 2;
-      let rad = Math.atan2(e.clientY - centerY, e.clientX - centerX);
-      let deg = rad * 180 / Math.PI + 90;
-      if (deg < 0) deg += 360;
-
-      const newTime = angleToTime(deg);
-      if (dragging === 'start') onChangeStart(newTime);
-      else onChangeEnd(newTime);
-    };
-    const handleUp = () => setDragging(null);
-    if (dragging) {
-      window.addEventListener('pointermove', handleMove);
-      window.addEventListener('pointerup', handleUp);
-    }
-    return () => {
-      window.removeEventListener('pointermove', handleMove);
-      window.removeEventListener('pointerup', handleUp);
-    };
-  }, [dragging]);
-
-  const ticks = Array.from({ length: 24 }).map((_, i) => {
-    const angle = i * 15;
-    const isMajor = i % 6 === 0;
-    const p1 = polarToCartesian(cx, cy, r - (isMajor ? 6 : 3), angle);
-    const p2 = polarToCartesian(cx, cy, r, angle);
-    const labelPos = polarToCartesian(cx, cy, r - 15, angle);
-    return (
-      <g key={i}>
-        <line x1={p1.x} y1={p1.y} x2={p2.x} y2={p2.y} stroke={isMajor ? "#94a3b8" : "#e2e8f0"} strokeWidth={isMajor ? 2 : 1} />
-        {isMajor && (
-          <text x={labelPos.x} y={labelPos.y} fill="#64748b" fontSize="9" textAnchor="middle" dominantBaseline="central" className="font-medium">
-            {i}
-          </text>
-        )}
-      </g>
-    );
-  });
-
-  return (
-    <div className="flex flex-col items-center justify-center bg-slate-50/50 p-2 rounded-xl border border-slate-100">
-      <svg ref={svgRef} width={size} height={size} className="touch-none select-none">
-        <circle cx={cx} cy={cy} r={r} fill="none" stroke="#f1f5f9" strokeWidth="8" />
-        {ticks}
-        <path d={getArcPath(cx, cy, r, startAngle, endAngle)} fill="none" stroke="#3b82f6" strokeWidth="8" strokeLinecap="round" />
-        
-        {/* 中心紧凑文字 */}
-        <text x={cx} y={cy - 8} textAnchor="middle" className="text-xs font-bold fill-slate-800">{startTime.slice(0, 5)}</text>
-        <text x={cx} y={cy + 4} textAnchor="middle" className="text-[10px] font-medium fill-slate-400">至</text>
-        <text x={cx} y={cy + 16} textAnchor="middle" className="text-xs font-bold fill-slate-800">{endTime.slice(0, 5)}</text>
-
-        <g onPointerDown={(e) => { e.preventDefault(); setDragging('start'); }} className="cursor-grab active:cursor-grabbing">
-          <circle cx={startPos.x} cy={startPos.y} r="18" fill="transparent" />
-          <circle cx={startPos.x} cy={startPos.y} r="6" fill="#10b981" stroke="#fff" strokeWidth="2" className="shadow-sm" />
-        </g>
-        <g onPointerDown={(e) => { e.preventDefault(); setDragging('end'); }} className="cursor-grab active:cursor-grabbing">
-          <circle cx={endPos.x} cy={endPos.y} r="18" fill="transparent" />
-          <circle cx={endPos.x} cy={endPos.y} r="6" fill="#ef4444" stroke="#fff" strokeWidth="2" className="shadow-sm" />
-        </g>
-      </svg>
-      <div className="flex gap-3 mt-1 text-[10px] font-bold text-slate-500">
-        <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>启动</span>
-        <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-red-500"></span>停止</span>
-      </div>
-    </div>
-  );
-}
-
-// ==========================================
-// 2. 主配置页面 (ContainerScheduleManager)
-// ==========================================
-interface Schedule {
-  container_name: string;
-  enabled: boolean;
-  days_mask: number;
-  start_time: string;
-  end_time: string;
-}
-
-const CONFIG_KEY = 'docker.schedules';
-const DAYS =[
-  { label: '一', bit: 1 }, { label: '二', bit: 2 }, { label: '三', bit: 4 },
-  { label: '四', bit: 8 }, { label: '五', bit: 16 }, { label: '六', bit: 32 }, { label: '日', bit: 64 }
-];
-
-export default function ContainerScheduleManager() {
-  const[schedules, setSchedules] = useState<Schedule[]>([]);
-  const [isExistRecord, setIsExistRecord] = useState(false);
-  const [loading, setLoading] = useState(false);
-  const [saving, setSaving] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-  const [localTz, setLocalTz] = useState<string>('');
-
-  useEffect(() => { setLocalTz(Intl.DateTimeFormat().resolvedOptions().timeZone); },[]);
-
-  // ====== 核心时间与掩码转换逻辑 ======
-  const formatTimeStr = (h: number, m: number, s: number) => `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
-  
-  const localToUtcTime = (timeStr: string) => {
-    if (!timeStr) return "00:00:00";
-    const [h, m, s] = timeStr.split(':').map(Number);
-    const d = new Date(); d.setHours(h, m, s || 0, 0);
-    return formatTimeStr(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds());
-  };
-
-  const utcToLocalTime = (timeStr: string) => {
-    if (!timeStr) return "00:00:00";
-    const [h, m, s] = timeStr.split(':').map(Number);
-    const d = new Date(); d.setUTCHours(h, m, s || 0, 0);
-    return formatTimeStr(d.getHours(), d.getMinutes(), d.getSeconds());
-  };
-
-  const getUtcDayOffset = (localTimeStr: string) => {
-    const [h, m, s] = (localTimeStr || "00:00:00").split(':').map(Number);
-    const d = new Date(); d.setHours(h, m, s || 0, 0);
-    let diff = d.getUTCDay() - d.getDay();
-    if (diff === 6) diff = -1; if (diff === -6) diff = 1;
-    return diff;
-  };
-
-  const getLocalDayOffset = (utcTimeStr: string) => {
-    const [h, m, s] = (utcTimeStr || "00:00:00").split(':').map(Number);
-    const d = new Date(); d.setUTCHours(h, m, s || 0, 0);
-    let diff = d.getDay() - d.getUTCDay();
-    if (diff === 6) diff = -1; if (diff === -6) diff = 1;
-    return diff;
-  };
-
-  const shiftDaysMask = (mask: number, offset: number) => {
-    if (offset === 0) return mask;
-    let newMask = 0; const bits =[1, 2, 4, 8, 16, 32, 64];
-    for (let i = 0; i < 7; i++) {
-      if ((mask & bits[i]) === bits[i]) {
-        const newIdx = (i + offset + 7) % 7;
-        newMask |= bits[newIdx];
-      }
-    }
-    return newMask;
-  };
-
-  // ====== 接口请求 ======
-  const fetchSchedules = async () => {
-    setLoading(true); setError(null);
-    try {
-      const res = await api.get(`/api/dynamic-configurations/key/${CONFIG_KEY}`);
-      if (res.data?.code === 0 && res.data?.data) {
-        setIsExistRecord(true);
-        let parsedData: any = { schedules:[] };
-        if (typeof res.data.data.config_value === 'string') {
-          try { parsedData = JSON.parse(res.data.data.config_value); } catch (e) {}
-        } else { parsedData = res.data.data.config_value; }
-
-        if (parsedData && Array.isArray(parsedData.schedules)) {
-          setSchedules(parsedData.schedules.map((sch: Schedule) => ({
-            ...sch,
-            start_time: utcToLocalTime(sch.start_time),
-            end_time: utcToLocalTime(sch.end_time),
-            days_mask: shiftDaysMask(sch.days_mask, getLocalDayOffset(sch.start_time))
-          })));
-        } else setSchedules([]);
-      }
-    } catch (err: any) {
-      if (err.response?.status === 404) { setIsExistRecord(false); setSchedules([]); }
-      else setError(err.response?.data?.message || '获取运行计划失败');
-    } finally { setLoading(false); }
-  };
-
-  useEffect(() => { fetchSchedules(); },[]);
-
-  const handleSave = async () => {
-    setSaving(true); setError(null);
-    try {
-      const utcSchedules = schedules.map((sch: Schedule) => ({
-        ...sch,
-        start_time: localToUtcTime(sch.start_time),
-        end_time: localToUtcTime(sch.end_time),
-        days_mask: shiftDaysMask(sch.days_mask, getUtcDayOffset(sch.start_time))
-      }));
-
-      const payload = { config_key: CONFIG_KEY, config_value: JSON.stringify({ schedules: utcSchedules }), description: "Docker 容器定时运行计划", type: "json" };
-      if (isExistRecord) await api.put(`/api/dynamic-configurations/key/${CONFIG_KEY}`, payload);
-      else { await api.post(`/api/dynamic-configurations`, payload); setIsExistRecord(true); }
-      alert('保存成功!服务端将以 UTC 时间按时执行。');
-    } catch (err: any) { setError(err.response?.data?.message || '保存失败'); }
-    finally { setSaving(false); }
-  };
-
-  const addSchedule = () => setSchedules([...schedules, { container_name: '', enabled: true, days_mask: 127, start_time: '08:00:00', end_time: '18:00:00' }]);
-  const removeSchedule = (index: number) => setSchedules(schedules.filter((_, i) => i !== index));
-  const updateSchedule = (index: number, field: keyof Schedule, value: any) => {
-    const newSchedules = [...schedules];
-    newSchedules[index] = { ...newSchedules[index], [field]: value };
-    setSchedules(newSchedules);
-  };
-  const handleTimeChange = (index: number, field: 'start_time' | 'end_time', value: string) => updateSchedule(index, field, value && value.length === 5 ? `${value}:00` : value);
-  const toggleDay = (index: number, bit: number) => {
-    const currentMask = schedules[index]?.days_mask || 0;
-    updateSchedule(index, 'days_mask', (currentMask & bit) === bit ? currentMask - bit : currentMask + bit);
-  };
-
-  if (loading) return <div className="p-8 flex justify-center text-slate-500"><RefreshCw className="animate-spin mr-2" />读取配置中...</div>;
-
-  return (
-    <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 space-y-4 animate-in fade-in">
-      
-      {/* 头部导航区域 */}
-      <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-2">
-        <div className="flex items-center gap-3">
-          <Clock size={18} className="text-blue-600" />
-          <h2 className="text-base font-bold text-slate-800">全局运行计划设置</h2>
-          {localTz && (
-            <div className="flex items-center gap-1 px-2 py-0.5 bg-slate-100 text-slate-500 text-[11px] rounded font-medium border border-slate-200">
-              <Globe size={10} /> {localTz}
-            </div>
-          )}
-        </div>
-        <button onClick={addSchedule} className="flex items-center gap-1 px-3 py-1.5 bg-blue-50 text-blue-600 text-xs font-bold rounded hover:bg-blue-100 transition-colors">
-          <Plus size={14} /> 添加计划
-        </button>
-      </div>
-
-      {error && <div className="bg-red-50 text-red-600 p-2.5 rounded-lg text-xs flex items-center gap-2"><AlertCircle size={14} /> {error}</div>}
-
-      {schedules.length === 0 ? (
-        <div className="text-center p-8 bg-slate-50 rounded-lg border border-dashed border-slate-200 text-slate-500 text-xs">
-          当前没有设置任何容器计划。
-        </div>
-      ) : (
-        <div className="space-y-3">
-          {schedules.map((schedule, index) => (
-            <div key={index} className="bg-white border border-slate-200 rounded-lg p-3 shadow-sm hover:border-blue-300 transition-colors">
-              <div className="flex flex-col sm:flex-row gap-4">
-                
-                {/* 1. 左侧缩略表盘 */}
-                <div className="flex justify-center shrink-0">
-                  <TimeRangeDial 
-                    startTime={schedule.start_time} endTime={schedule.end_time} 
-                    onChangeStart={(val) => handleTimeChange(index, 'start_time', val)}
-                    onChangeEnd={(val) => handleTimeChange(index, 'end_time', val)}
-                  />
-                </div>
-
-                {/* 2. 右侧紧凑表单 */}
-                <div className="flex-1 flex flex-col justify-between space-y-3">
-                  
-                  {/* 首行: 容器名称支持通配符 */}
-                  <div className="space-y-1">
-                    <label className="text-[11px] font-bold text-slate-500 uppercase flex items-center gap-2">
-                      <span>容器名称</span>
-                      <span className="flex items-center gap-0.5 text-blue-500 font-normal normal-case bg-blue-50 px-1.5 py-0.5 rounded text-[10px]">
-                        <Info size={10} /> 支持 * 通配符
-                      </span>
-                    </label>
-                    <input
-                      type="text"
-                      value={schedule.container_name || ''}
-                      onChange={(e) => updateSchedule(index, 'container_name', e.target.value)}
-                      className="w-full px-2.5 py-1.5 border border-slate-300 rounded text-sm focus:ring-1 focus:ring-blue-500 outline-none"
-                      placeholder="例如: api-server-* 或 worker-node-?"
-                    />
-                  </div>
-
-                  {/* 次行: 起止时间 */}
-                  <div className="grid grid-cols-2 gap-3">
-                    <div className="space-y-1">
-                      <label className="text-[10px] font-bold text-slate-500 uppercase">启动时间 (本地)</label>
-                      <input type="time" step="60" value={schedule.start_time || '00:00:00'} onChange={(e) => handleTimeChange(index, 'start_time', e.target.value)} className="w-full px-2 py-1.5 border border-slate-300 rounded text-sm outline-none focus:ring-1 focus:ring-blue-500" />
-                    </div>
-                    <div className="space-y-1">
-                      <label className="text-[10px] font-bold text-slate-500 uppercase">停止时间 (本地)</label>
-                      <input type="time" step="60" value={schedule.end_time || '00:00:00'} onChange={(e) => handleTimeChange(index, 'end_time', e.target.value)} className="w-full px-2 py-1.5 border border-slate-300 rounded text-sm outline-none focus:ring-1 focus:ring-blue-500" />
-                    </div>
-                  </div>
-
-                  {/* 尾行: 星期掩码 + 开关 + 删除 (高度压缩到同一行) */}
-                  <div className="flex flex-wrap items-center justify-between gap-3 pt-2.5 border-t border-slate-100">
-                    <div className="flex gap-1.5">
-                      {DAYS.map((day) => {
-                        const isSelected = ((schedule.days_mask || 0) & day.bit) === day.bit;
-                        return (
-                          <button
-                            key={day.bit} onClick={() => toggleDay(index, day.bit)}
-                            className={`w-7 h-7 rounded text-[11px] font-bold transition-all border ${isSelected ? 'bg-blue-600 text-white border-blue-600' : 'bg-slate-50 text-slate-500 border-slate-200 hover:border-blue-400'}`}
-                          >
-                            {day.label}
-                          </button>
-                        );
-                      })}
-                    </div>
-                    
-                    <div className="flex items-center gap-4 ml-auto">
-                      <label className="flex items-center gap-1.5 cursor-pointer">
-                        <input type="checkbox" checked={!!schedule.enabled} onChange={(e) => updateSchedule(index, 'enabled', e.target.checked)} className="w-3.5 h-3.5 text-blue-600 rounded cursor-pointer" />
-                        <span className="text-xs font-bold text-slate-700">启用</span>
-                      </label>
-                      <button onClick={() => removeSchedule(index)} className="text-red-400 hover:text-red-600 transition-colors p-1" title="删除该计划">
-                        <Trash2 size={16} />
-                      </button>
-                    </div>
-                  </div>
-
-                </div>
-              </div>
-            </div>
-          ))}
-        </div>
-      )}
-
-      {/* 底部保存按钮 */}
-      <div className="pt-4 mt-2 border-t border-slate-100 flex justify-end">
-        <button onClick={handleSave} disabled={saving || schedules.length === 0} className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white text-sm font-bold rounded hover:bg-blue-700 transition-all disabled:opacity-50">
-          {saving ? <RefreshCw size={16} className="animate-spin" /> : <Save size={16} />}
-          {saving ? '保存中...' : '保存所有计划'}
-        </button>
-      </div>
-    </div>
-  );
-}

+ 0 - 274
src/components/admin/remote-server/DockerControl.tsx

@@ -1,274 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { Play, Square, RotateCw, RefreshCw, FileText, AlertCircle } from 'lucide-react';
-
-interface ServerConfig {
-  host: string;
-  port: number;
-  username: string;
-  password?: string;
-  key_file?: string;
-  project_path: string;
-}
-
-interface ContainerStatus {
-  name: string;
-  status: string;
-  image: string;
-}
-
-export default function DockerControl({ 
-  serverConfig, 
-  serverId,
-  projectPath
-}: { 
-  serverConfig: ServerConfig;
-  serverId?: string;
-  projectPath?: string;
-}) {
-  const [containers, setContainers] = useState<Record<string, ContainerStatus>>({});
-  const [loading, setLoading] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-
-  const fetchStatus = async () => {
-    setLoading(true);
-    setError(null);
-    try {
-      const url = serverId ? '/api/remote/server/docker/status' : '/api/remote/docker/status';
-      const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        setContainers(response.data.data.containers || {});
-      } else {
-        setError(response.data.message || '获取状态失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '请求失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    fetchStatus();
-  }, [serverId, projectPath]);
-
-  const handleDockerAction = async (action: 'start' | 'stop' | 'restart', containerName: string) => {
-    setLoading(true);
-    setError(null);
-    try {
-      const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
-      const payload = serverId 
-        ? { server_id: serverId, container_name: containerName, project_path: projectPath }
-        : { ...serverConfig, container_name: containerName, project_path: projectPath };
-        
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        alert('操作成功');
-        await fetchStatus();
-      } else {
-        setError(response.data.message || '操作失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '操作失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const handleComposeAction = async (action: 'up' | 'down') => {
-    setLoading(true);
-    setError(null);
-    try {
-      const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
-      const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        alert('操作成功');
-        await fetchStatus();
-      } else {
-        setError(response.data.message || '操作失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '操作失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  return (
-    <div className="space-y-6">
-      {/* 操作栏 */}
-      <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
-        <div className="flex items-center gap-2">
-          <h3 className="text-lg font-semibold text-slate-800">Docker 容器管理</h3>
-          <button
-            onClick={fetchStatus}
-            disabled={loading}
-            className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors disabled:opacity-50"
-            title="刷新状态"
-          >
-            <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
-          </button>
-        </div>
-        <div className="grid grid-cols-2 sm:flex gap-2">
-          <button
-            onClick={() => handleComposeAction('up')}
-            disabled={loading}
-            className="px-3 sm:px-4 py-2 bg-green-600 text-white text-sm sm:text-base rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
-          >
-            <Play size={18} />
-            <span className="whitespace-nowrap">全部启动</span>
-          </button>
-          <button
-            onClick={() => handleComposeAction('down')}
-            disabled={loading}
-            className="px-3 sm:px-4 py-2 bg-red-600 text-white text-sm sm:text-base rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
-          >
-            <Square size={18} />
-            <span className="whitespace-nowrap">全部停止</span>
-          </button>
-        </div>
-      </div>
-
-      {error && (
-        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md flex items-center gap-2">
-          <AlertCircle size={18} />
-          {error}
-        </div>
-      )}
-
-      {/* 容器列表 */}
-      <div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
-        {/* 桌面端表格视图 */}
-        <div className="hidden md:block overflow-x-auto">
-          <table className="w-full">
-            <thead className="bg-slate-50 border-b border-slate-200">
-              <tr>
-                <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">容器名称</th>
-                <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">状态</th>
-                <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">镜像</th>
-                <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">操作</th>
-              </tr>
-            </thead>
-            <tbody className="divide-y divide-slate-200">
-              {Object.keys(containers).length === 0 ? (
-                <tr>
-                  <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
-                    {loading ? '加载中...' : '暂无容器'}
-                  </td>
-                </tr>
-              ) : (
-                Object.entries(containers).map(([name, container]) => (
-                  <tr key={name} className="hover:bg-slate-50">
-                    <td className="px-4 py-3 text-sm text-slate-800 font-medium">{name}</td>
-                    <td className="px-4 py-3 text-sm">
-                      <span
-                        className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${
-                          container.status.includes('Up')
-                            ? 'bg-green-100 text-green-800'
-                            : 'bg-red-100 text-red-800'
-                        }`}
-                      >
-                        {container.status}
-                      </span>
-                    </td>
-                    <td className="px-4 py-3 text-sm text-slate-600">{container.image}</td>
-                    <td className="px-4 py-3 text-right">
-                      <div className="flex items-center justify-end gap-2">
-                        <button
-                          onClick={() => handleDockerAction('start', name)}
-                          disabled={loading || container.status.includes('Up')}
-                          className="p-1.5 text-green-600 hover:bg-green-50 rounded transition-colors disabled:opacity-50"
-                          title="启动"
-                        >
-                          <Play size={16} />
-                        </button>
-                        <button
-                          onClick={() => handleDockerAction('stop', name)}
-                          disabled={loading || !container.status.includes('Up')}
-                          className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors disabled:opacity-50"
-                          title="停止"
-                        >
-                          <Square size={16} />
-                        </button>
-                        <button
-                          onClick={() => handleDockerAction('restart', name)}
-                          disabled={loading}
-                          className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors disabled:opacity-50"
-                          title="重启"
-                        >
-                          <RotateCw size={16} />
-                        </button>
-                      </div>
-                    </td>
-                  </tr>
-                ))
-              )}
-            </tbody>
-          </table>
-        </div>
-
-        {/* 移动端卡片视图 */}
-        <div className="md:hidden divide-y divide-slate-200">
-          {Object.keys(containers).length === 0 ? (
-            <div className="px-4 py-8 text-center text-slate-500">
-              {loading ? '加载中...' : '暂无容器'}
-            </div>
-          ) : (
-            Object.entries(containers).map(([name, container]) => (
-              <div key={name} className="p-4 space-y-3">
-                <div className="flex items-center justify-between">
-                  <span className="text-sm font-semibold text-slate-800">{name}</span>
-                  <span
-                    className={`inline-flex px-2 py-0.5 rounded-full text-[10px] font-medium ${
-                      container.status.includes('Up')
-                        ? 'bg-green-100 text-green-800'
-                        : 'bg-red-100 text-red-800'
-                    }`}
-                  >
-                    {container.status}
-                  </span>
-                </div>
-                <div className="text-xs text-slate-500 truncate">
-                  <span className="font-medium text-slate-600 mr-1">镜像:</span>
-                  {container.image}
-                </div>
-                <div className="flex items-center justify-between pt-2 border-t border-slate-100">
-                  <div className="flex gap-4">
-                    <button
-                      onClick={() => handleDockerAction('start', name)}
-                      disabled={loading || container.status.includes('Up')}
-                      className="flex flex-col items-center gap-1 text-green-600 disabled:opacity-30"
-                    >
-                      <Play size={18} />
-                      <span className="text-[10px]">启动</span>
-                    </button>
-                    <button
-                      onClick={() => handleDockerAction('stop', name)}
-                      disabled={loading || !container.status.includes('Up')}
-                      className="flex flex-col items-center gap-1 text-red-600 disabled:opacity-30"
-                    >
-                      <Square size={18} />
-                      <span className="text-[10px]">停止</span>
-                    </button>
-                    <button
-                      onClick={() => handleDockerAction('restart', name)}
-                      disabled={loading}
-                      className="flex flex-col items-center gap-1 text-blue-600 disabled:opacity-30"
-                    >
-                      <RotateCw size={18} />
-                      <span className="text-[10px]">重启</span>
-                    </button>
-                  </div>
-                </div>
-              </div>
-            ))
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 253
src/components/admin/remote-server/LogViewer.tsx

@@ -1,253 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { FileText, Search, RefreshCw, AlertCircle, Download } from 'lucide-react';
-
-interface ServerConfig {
-  host: string;
-  port: number;
-  username: string;
-  password?: string;
-  key_file?: string;
-  project_path: string;
-}
-
-export default function LogViewer({ 
-  serverConfig,
-  serverId,
-  projectPath
-}: { 
-  serverConfig: ServerConfig;
-  serverId?: string;
-  projectPath?: string;
-}) {
-  const [logFiles, setLogFiles] = useState<string[]>([]);
-  const [selectedLog, setSelectedLog] = useState<string>('');
-  const [logContent, setLogContent] = useState<string>('');
-  const [lines, setLines] = useState(100);
-  const [fromHead, setFromHead] = useState(false);
-  const [full, setFull] = useState(false);
-  const [loading, setLoading] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-
-  const fetchLogList = async () => {
-    setLoading(true);
-    setError(null);
-    try {
-      const url = serverId ? '/api/remote/server/log/list' : '/api/remote/log/list';
-      const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        setLogFiles(response.data.data.log_files || []);
-      } else {
-        setError(response.data.message || '获取日志列表失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '请求失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const fetchLogContent = async () => {
-    if (!selectedLog) return;
-    
-    setLoading(true);
-    setError(null);
-    try {
-      const url = serverId ? '/api/remote/server/log/read' : '/api/remote/log/read';
-      const payload = serverId 
-        ? { 
-            server_id: serverId, 
-            log_file: selectedLog,
-            lines: full ? 100 : lines,
-            from_head: fromHead,
-            full: full,
-            project_path: projectPath
-          }
-        : {
-            ...serverConfig,
-            log_file: selectedLog,
-            lines: full ? 100 : lines,
-            from_head: fromHead,
-            full: full,
-            project_path: projectPath,
-          };
-
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        setLogContent(response.data.data.content || '');
-      } else {
-        setError(response.data.message || '读取日志失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '读取日志失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  useEffect(() => {
-    fetchLogList();
-  }, [serverId, projectPath]);
-
-  useEffect(() => {
-    if (selectedLog) {
-      fetchLogContent();
-    }
-  }, [selectedLog, lines, fromHead, full]);
-
-  const downloadLog = () => {
-    if (!logContent) return;
-    const blob = new Blob([logContent], { type: 'text/plain' });
-    const url = URL.createObjectURL(blob);
-    const a = document.createElement('a');
-    a.href = url;
-    a.download = selectedLog || 'log.txt';
-    a.click();
-    URL.revokeObjectURL(url);
-  };
-
-  return (
-    <div className="space-y-4 sm:space-y-6">
-      <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
-        <h3 className="text-lg font-semibold text-slate-800">日志文件查看</h3>
-        <button
-          onClick={fetchLogList}
-          disabled={loading}
-          className="w-full sm:w-auto px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
-        >
-          <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
-          刷新列表
-        </button>
-      </div>
-
-      {error && (
-        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md flex items-center gap-2 text-sm">
-          <AlertCircle size={18} />
-          {error}
-        </div>
-      )}
-
-      <div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
-        {/* 日志文件列表 */}
-        <div className="bg-slate-50 rounded-lg border border-slate-200 p-4">
-          <h4 className="text-sm font-semibold text-slate-700 mb-3">日志文件列表</h4>
-          <div className="space-y-1 max-h-48 sm:max-h-96 overflow-y-auto">
-            {logFiles.length === 0 ? (
-              <p className="text-sm text-slate-500 text-center py-4">暂无日志文件</p>
-            ) : (
-              logFiles.map((file) => (
-                <button
-                  key={file}
-                  onClick={() => setSelectedLog(file)}
-                  className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors truncate ${
-                    selectedLog === file
-                      ? 'bg-blue-600 text-white'
-                      : 'bg-white text-slate-700 hover:bg-slate-100'
-                  }`}
-                >
-                  <FileText size={16} className="inline mr-2 shrink-0" />
-                  {file}
-                </button>
-              ))
-            )}
-          </div>
-        </div>
-
-        {/* 日志内容 */}
-        <div className="lg:col-span-2 space-y-4">
-          {selectedLog ? (
-            <>
-              {/* 控制栏 */}
-              <div className="bg-white rounded-lg border border-slate-200 p-3 sm:p-4">
-                <div className="flex flex-wrap items-center gap-3 sm:gap-4">
-                  <div className="flex items-center gap-2">
-                    <label className="text-xs sm:text-sm text-slate-700 whitespace-nowrap">行数:</label>
-                    <input
-                      type="number"
-                      value={lines}
-                      onChange={(e) => {
-                        setLines(parseInt(e.target.value) || 100);
-                        setFull(false);
-                      }}
-                      disabled={full || loading}
-                      className="w-16 sm:w-20 px-2 py-1 border border-slate-300 rounded text-xs sm:text-sm disabled:opacity-50"
-                    />
-                  </div>
-                  <div className="flex items-center gap-4">
-                    <label className="flex items-center gap-1.5 text-xs sm:text-sm text-slate-700 cursor-pointer">
-                      <input
-                        type="checkbox"
-                        checked={fromHead}
-                        onChange={(e) => {
-                          setFromHead(e.target.checked);
-                          setFull(false);
-                        }}
-                        disabled={full || loading}
-                        className="rounded text-blue-600 focus:ring-blue-500"
-                      />
-                      从开头
-                    </label>
-                    <label className="flex items-center gap-1.5 text-xs sm:text-sm text-slate-700 cursor-pointer">
-                      <input
-                        type="checkbox"
-                        checked={full}
-                        onChange={(e) => {
-                          setFull(e.target.checked);
-                          if (e.target.checked) {
-                            setFromHead(false);
-                          }
-                        }}
-                        disabled={loading}
-                        className="rounded text-blue-600 focus:ring-blue-500"
-                      />
-                      全部
-                    </label>
-                  </div>
-                  <div className="flex items-center gap-2 w-full sm:w-auto">
-                    <button
-                      onClick={fetchLogContent}
-                      disabled={loading}
-                      className="flex-1 sm:flex-none px-3 py-1.5 bg-blue-600 text-white text-xs sm:text-sm rounded hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-1.5"
-                    >
-                      <RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
-                      刷新
-                    </button>
-                    <button
-                      onClick={downloadLog}
-                      disabled={!logContent}
-                      className="flex-1 sm:flex-none px-3 py-1.5 bg-green-600 text-white text-xs sm:text-sm rounded hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-1.5"
-                    >
-                      <Download size={14} />
-                      下载
-                    </button>
-                  </div>
-                </div>
-              </div>
-
-              {/* 日志内容显示 */}
-              <div className="bg-slate-900 rounded-lg border border-slate-200 p-3 sm:p-4">
-                <div className="flex items-center justify-between mb-2">
-                  <h4 className="text-xs sm:text-sm font-semibold text-white truncate mr-2">{selectedLog}</h4>
-                  <span className="text-[10px] sm:text-xs text-slate-400 whitespace-nowrap">
-                    {logContent.split('\n').length} 行
-                  </span>
-                </div>
-                <pre className="text-[10px] sm:text-xs text-green-400 font-mono overflow-x-auto max-h-[400px] sm:max-h-[600px] overflow-y-auto whitespace-pre-wrap">
-                  {logContent || (loading ? '加载中...' : '暂无内容')}
-                </pre>
-              </div>
-            </>
-          ) : (
-            <div className="bg-slate-50 rounded-lg border border-slate-200 p-6 sm:p-8 text-center">
-              <FileText className="mx-auto text-slate-400 mb-3 w-8 h-8 sm:w-12 sm:h-12" />
-              <p className="text-sm sm:text-base text-slate-500">请选择一个日志文件查看</p>
-            </div>
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 362
src/components/admin/remote-server/RemoteServerControl.tsx

@@ -1,362 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { Server, Play, RotateCw, FileText, Settings, Search, RefreshCw, CheckCircle2, AlertCircle } from 'lucide-react';
-import DockerControl from './DockerControl';
-import LogViewer from './LogViewer';
-import ConfigManager from './ConfigManager';
-
-interface ServerConfig {
-  host: string;
-  port: number;
-  username: string;
-  password?: string;
-  key_file?: string;
-  project_path: string;
-}
-
-interface PreConfiguredServer {
-  id: string;
-  name: string;
-  host: string;
-}
-
-export default function RemoteServerControl() {
-  const [activeTab, setActiveTab] = useState<'docker' | 'logs' | 'config'>('docker');
-  const [usePreConfigured, setUsePreConfigured] = useState(true);
-  const [preConfiguredServers, setPreConfiguredServers] = useState<PreConfiguredServer[]>([]);
-  const [selectedServerId, setSelectedServerId] = useState<string>('');
-  const [selectedPath, setSelectedPath] = useState<string>('/root/troov-asyncio');
-  const [serverConfig, setServerConfig] = useState<ServerConfig>({
-    host: '',
-    port: 22,
-    username: 'root',
-    password: '',
-    key_file: '',
-    project_path: '/root/troov-asyncio',
-  });
-  const [isConnected, setIsConnected] = useState(false);
-  const [connecting, setConnecting] = useState(false);
-  const [connectionError, setConnectionError] = useState<string | null>(null);
-
-  useEffect(() => {
-    const fetchServers = async () => {
-      try {
-        const response = await api.get('/api/remote/servers');
-        if (response.data.code === 0) {
-          const servers = response.data.data.servers || [];
-          setPreConfiguredServers(servers);
-          if (servers.length > 0) setSelectedServerId(servers[0].id);
-        }
-      } catch (err) {
-        console.error('获取服务器列表失败:', err);
-      }
-    };
-    fetchServers();
-  }, []);
-
-  const handleConnect = async () => {
-    setConnecting(true);
-    setConnectionError(null);
-    try {
-      let response;
-      if (usePreConfigured) {
-        if (!selectedServerId) {
-          setConnectionError('请选择服务器');
-          setConnecting(false);
-          return;
-        }
-        response = await api.post('/api/remote/server/docker/status', { 
-          server_id: selectedServerId,
-          project_path: selectedPath
-        });
-      } else {
-        if (!serverConfig.host || !serverConfig.username) {
-          setConnectionError('请填写服务器地址和用户名');
-          setConnecting(false);
-          return;
-        }
-        response = await api.post('/api/remote/docker/status', {
-          ...serverConfig,
-          project_path: selectedPath
-        });
-      }
-      
-      if (response.data.code === 0) {
-        setIsConnected(true);
-        setConnectionError(null);
-      } else {
-        setConnectionError(response.data.message || '连接失败');
-        setIsConnected(false);
-      }
-    } catch (err: any) {
-      console.error('连接错误详情:', err);
-      let errorMsg = '连接失败,请检查配置';
-      if (err.response) {
-        const status = err.response.status;
-        if (status === 404) errorMsg = 'API 路由未找到 (404)';
-        else if (status === 401) errorMsg = '未授权 (401)';
-        else if (err.response.data?.message) errorMsg = err.response.data.message;
-      }
-      setConnectionError(errorMsg);
-      setIsConnected(false);
-    } finally {
-      setConnecting(false);
-    }
-  };
-
-  const handleDisconnect = () => {
-    setIsConnected(false);
-    setConnectionError(null);
-  };
-
-  return (
-    <div className="space-y-4 md:space-y-6 p-4 md:p-0">
-      
-      {/* === 卡片 1: 服务器连接配置 === */}
-      <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
-        
-        {/* 卡片头部 */}
-        <div className="p-4 md:p-6 border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
-          <div className="flex items-center gap-3">
-            <div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
-              <Server size={20} />
-            </div>
-            <h2 className="text-lg font-bold text-slate-800">服务器连接</h2>
-          </div>
-
-          {/* 切换开关:移动端全宽 */}
-          <div className="flex bg-slate-100 p-1 rounded-lg w-full md:w-auto">
-            <button
-              onClick={() => !isConnected && setUsePreConfigured(true)}
-              className={`flex-1 md:flex-none px-4 py-2 text-xs font-bold rounded-md transition-all ${
-                usePreConfigured ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'
-              }`}
-              disabled={isConnected}
-            >
-              预配置列表
-            </button>
-            <button
-              onClick={() => !isConnected && setUsePreConfigured(false)}
-              className={`flex-1 md:flex-none px-4 py-2 text-xs font-bold rounded-md transition-all ${
-                !usePreConfigured ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'
-              }`}
-              disabled={isConnected}
-            >
-              手动输入
-            </button>
-          </div>
-        </div>
-        
-        {/* 卡片内容 */}
-        <div className="p-4 md:p-6 space-y-5">
-          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
-            {usePreConfigured ? (
-              <div className="space-y-1">
-                <label className="block text-xs font-bold text-slate-500 uppercase mb-2">选择目标服务器</label>
-                <div className="relative">
-                  <select
-                    value={selectedServerId}
-                    onChange={(e) => setSelectedServerId(e.target.value)}
-                    className="w-full pl-4 pr-10 py-3 border border-slate-300 rounded-xl appearance-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-white"
-                    disabled={isConnected}
-                  >
-                    <option value="">-- 请选择服务器 --</option>
-                    {preConfiguredServers.map((server) => (
-                      <option key={server.id} value={server.id}>
-                        {server.name} ({server.host})
-                      </option>
-                    ))}
-                  </select>
-                  <div className="absolute right-3 top-3.5 text-slate-400 pointer-events-none">
-                    <Search size={16} />
-                  </div>
-                </div>
-              </div>
-            ) : (
-              // 手动输入模式:移动端单列,桌面端双列
-              <>
-                <div className="space-y-1">
-                  <label className="text-xs font-bold text-slate-500">IP 地址</label>
-                  <input
-                    type="text"
-                    value={serverConfig.host}
-                    onChange={(e) => setServerConfig({ ...serverConfig, host: e.target.value })}
-                    placeholder="192.168.1.100"
-                    className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                    disabled={isConnected}
-                  />
-                </div>
-                <div className="space-y-1">
-                  <label className="text-xs font-bold text-slate-500">端口</label>
-                  <input
-                    type="number"
-                    value={serverConfig.port}
-                    onChange={(e) => setServerConfig({ ...serverConfig, port: parseInt(e.target.value) || 22 })}
-                    className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                    disabled={isConnected}
-                  />
-                </div>
-              </>
-            )}
-
-            <div className="space-y-1">
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-2">项目路径</label>
-              <div className="relative">
-                <select
-                  value={selectedPath}
-                  onChange={(e) => setSelectedPath(e.target.value)}
-                  className="w-full pl-4 pr-10 py-3 border border-slate-300 rounded-xl appearance-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-white"
-                  disabled={isConnected}
-                >
-                  <option value="/root/troov-asyncio">默认项目 (/root/troov-asyncio)</option>
-                  <option value="/root/germany_visa">德国签证 (/root/germany_visa)</option>
-                </select>
-                <div className="absolute right-3 top-3.5 text-slate-400 pointer-events-none">
-                  <Settings size={16} />
-                </div>
-              </div>
-            </div>
-          </div>
-
-          {!usePreConfigured && (
-            <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t border-slate-50">
-              <div className="space-y-1">
-                <label className="text-xs font-bold text-slate-500">用户名</label>
-                <input
-                  type="text"
-                  value={serverConfig.username}
-                  onChange={(e) => setServerConfig({ ...serverConfig, username: e.target.value })}
-                  placeholder="root"
-                  className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                  disabled={isConnected}
-                />
-              </div>
-              <div className="space-y-1">
-                <label className="text-xs font-bold text-slate-500">密码</label>
-                <input
-                  type="password"
-                  value={serverConfig.password || ''}
-                  onChange={(e) => setServerConfig({ ...serverConfig, password: e.target.value })}
-                  className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                  disabled={isConnected}
-                />
-              </div>
-            </div>
-          )}
-          
-          {/* 状态反馈区 */}
-          {connectionError && (
-            <div className="bg-red-50 border border-red-100 text-red-700 px-4 py-3 rounded-lg text-sm flex items-start gap-2">
-              <AlertCircle size={18} className="shrink-0 mt-0.5" />
-              <span className="break-all">{connectionError}</span>
-            </div>
-          )}
-          {isConnected && (
-            <div className="bg-green-50 border border-green-100 text-green-700 px-4 py-3 rounded-lg text-sm flex items-center gap-2 font-medium">
-              <CheckCircle2 size={18} />
-              已成功连接到服务器
-            </div>
-          )}
-
-          {/* 操作按钮:移动端垂直排列全宽,桌面端水平 */}
-          <div className="flex flex-col sm:flex-row gap-3 pt-2">
-            {!isConnected ? (
-              <>
-                <button
-                  onClick={handleConnect}
-                  disabled={connecting}
-                  className="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white text-sm font-bold rounded-xl hover:bg-blue-700 transition-all flex justify-center items-center gap-2 disabled:opacity-70 active:scale-95 shadow-md shadow-blue-100"
-                >
-                  {connecting ? <RefreshCw size={18} className="animate-spin" /> : <Server size={18} />}
-                  {connecting ? '正在连接...' : '立即连接'}
-                </button>
-                <button
-                  onClick={async () => {
-                    try {
-                      const res = await api.get('/api/ping');
-                      alert(`API OK: ${JSON.stringify(res.data)}`);
-                    } catch (err: any) {
-                      alert(`API Error: ${err.message}`);
-                    }
-                  }}
-                  className="w-full sm:w-auto px-6 py-3 bg-slate-100 text-slate-600 text-sm font-bold rounded-xl hover:bg-slate-200 transition-all flex justify-center items-center gap-2 active:scale-95"
-                >
-                  <RefreshCw size={18} /> 测试 API
-                </button>
-              </>
-            ) : (
-              <button
-                onClick={handleDisconnect}
-                className="w-full sm:w-auto px-6 py-3 bg-red-50 text-red-600 border border-red-100 text-sm font-bold rounded-xl hover:bg-red-100 transition-all flex justify-center items-center gap-2 active:scale-95"
-              >
-                <Server size={18} /> 断开连接
-              </button>
-            )}
-          </div>
-        </div>
-      </div>
-
-      {/* === 卡片 2: 功能控制区 (连接后显示) === */}
-      {isConnected && (
-        <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden animate-in fade-in slide-in-from-bottom-4">
-          
-          {/* Tab 导航:移动端支持横向滚动 */}
-          <div className="border-b border-slate-100 overflow-x-auto scrollbar-hide">
-            <nav className="flex min-w-max px-2">
-              <button
-                onClick={() => setActiveTab('docker')}
-                className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
-                  activeTab === 'docker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
-                }`}
-              >
-                <Play size={18} /> Docker 控制
-              </button>
-              <button
-                onClick={() => setActiveTab('logs')}
-                className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
-                  activeTab === 'logs' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
-                }`}
-              >
-                <FileText size={18} /> 日志查看
-              </button>
-              <button
-                onClick={() => setActiveTab('config')}
-                className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
-                  activeTab === 'config' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
-                }`}
-              >
-                <Settings size={18} /> 配置文件
-              </button>
-            </nav>
-          </div>
-          
-          <div className="p-4 md:p-6">
-            {activeTab === 'docker' && (
-              <DockerControl 
-                serverConfig={serverConfig} 
-                serverId={usePreConfigured ? selectedServerId : undefined} 
-                projectPath={selectedPath}
-              />
-            )}
-            {activeTab === 'logs' && (
-              <LogViewer 
-                serverConfig={serverConfig} 
-                serverId={usePreConfigured ? selectedServerId : undefined} 
-                projectPath={selectedPath}
-              />
-            )}
-            {activeTab === 'config' && (
-              <ConfigManager 
-                serverConfig={serverConfig} 
-                serverId={usePreConfigured ? selectedServerId : undefined} 
-                projectPath={selectedPath}
-              />
-            )}
-          </div>
-        </div>
-      )}
-    </div>
-  );
-}

+ 0 - 163
src/components/admin/slots/DailySlotDashboard.tsx

@@ -1,163 +0,0 @@
-'use client';
-
-import { useState, useMemo } from 'react';
-import { 
-  Users, AlertTriangle, CheckCircle, Clock, 
-  ChevronDown, ChevronUp, User, Mail, Hash 
-} from 'lucide-react';
-import LocalTime from '@/components/common/LocalTime';
-
-interface SlotCapacity {
-  time: string;
-  rate: string | number;
-  capacity: number;
-}
-
-// 扩展接口以支持完整 Task 结构
-export interface GrabbedTask {
-  id: number;
-  order_id: string;
-  grabbed_history?: any;
-  user_inputs?: any;
-  [key: string]: any; // 允许其他字段
-}
-
-interface DailySlotDashboardProps {
-  date: string;
-  capacityData: SlotCapacity[];
-  grabbedTasks: GrabbedTask[];
-  loading: boolean;
-  onTaskClick: (task: GrabbedTask, target: HTMLElement) => void;
-}
-
-export default function DailySlotDashboard({ date, capacityData, grabbedTasks, loading, onTaskClick }: DailySlotDashboardProps) {
-  
-  const mergedSlots = useMemo(() => {
-    const slotMap = new Map<string, { capacityInfo: SlotCapacity | null; tasks: GrabbedTask[] }>();
-
-    // 1. 容量
-    capacityData.forEach(item => slotMap.set(item.time, { capacityInfo: item, tasks: [] }));
-
-    // 2. 任务
-    grabbedTasks.forEach(task => {
-      // 兼容对象或数组形式的 history
-      let bookDateStr = null;
-      if (task.grabbed_history && !Array.isArray(task.grabbed_history)) {
-          bookDateStr = task.grabbed_history.book_date;
-      }
-      
-      if (!bookDateStr) return;
-
-      const bookDate = new Date(bookDateStr);
-      const taskDateStr = bookDate.toLocaleDateString('en-CA');
-      
-      if (taskDateStr === date) {
-         const timeKey = bookDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
-         if (!slotMap.has(timeKey)) slotMap.set(timeKey, { capacityInfo: null, tasks: [] });
-         slotMap.get(timeKey)!.tasks.push(task);
-      }
-    });
-
-    return Array.from(slotMap.entries())
-      .map(([time, data]) => ({ time, ...data }))
-      .sort((a, b) => a.time.localeCompare(b.time));
-
-  }, [date, capacityData, grabbedTasks]);
-
-  if (loading) return <div className="p-12 text-center text-gray-400">Loading...</div>;
-  if (mergedSlots.length === 0) return <div className="p-12 text-center border-2 border-dashed rounded text-slate-400">暂无数据</div>;
-
-  return (
-    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
-      {mergedSlots.map(({ time, capacityInfo, tasks }) => {
-        const rateVal = capacityInfo ? Number(capacityInfo.rate) : 0;
-        const isHighRisk = rateVal > 100;
-        const hasCapacity = capacityInfo && capacityInfo.capacity > 0;
-        const grabCount = tasks.length;
-        
-        let borderColor = "border-slate-200";
-        let bgColor = "bg-white";
-        let headerColor = "bg-slate-50 text-slate-700";
-
-        if (isHighRisk) {
-          borderColor = "border-red-200 ring-1 ring-red-100";
-          bgColor = "bg-red-50/30";
-          headerColor = "bg-red-100 text-red-800";
-        } else if (hasCapacity) {
-          borderColor = "border-emerald-200";
-          bgColor = "bg-emerald-50/30";
-          headerColor = "bg-emerald-100 text-emerald-800";
-        }
-
-        return (
-          <div key={time} className={`rounded-xl border ${borderColor} ${bgColor} overflow-hidden flex flex-col`}>
-            
-            {/* Header */}
-            <div className={`px-4 py-3 flex justify-between items-center ${headerColor}`}>
-              <div className="flex items-center gap-2">
-                <Clock size={16} />
-                <span className="text-lg font-mono font-bold">{time}</span>
-              </div>
-              {capacityInfo ? (
-                <div className="flex items-center gap-3 text-xs">
-                  <div className="flex flex-col items-end">
-                    <span className="font-bold">{capacityInfo.capacity} Slots</span>
-                    <span className={`${isHighRisk ? 'text-red-600 font-bold' : 'opacity-70'}`}>{rateVal}% Rate</span>
-                  </div>
-                  {isHighRisk && <AlertTriangle size={18} className="text-red-600 animate-pulse" />}
-                </div>
-              ) : <span className="text-xs opacity-50">Unknown Cap</span>}
-            </div>
-
-            {/* Content */}
-            <div className="p-4 flex-1 flex flex-col">
-              <div className="flex justify-between items-center mb-3">
-                <span className="text-xs font-bold text-slate-500 uppercase tracking-wide">Grabbed Result</span>
-                <span className={`text-xs font-bold px-2 py-0.5 rounded-full ${grabCount > 0 ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-400'}`}>
-                  {grabCount} Users
-                </span>
-              </div>
-
-              <div className="space-y-2 max-h-[200px] overflow-y-auto pr-1 custom-scrollbar">
-                {grabCount === 0 ? (
-                  <div className="text-center py-4 text-xs text-slate-400 italic">无人抢到该时段</div>
-                ) : (
-                  tasks.map(task => (
-                    <div 
-                      key={task.id} 
-                      onClick={(e) => onTaskClick(task, e.currentTarget)} 
-                      className="bg-white border border-slate-100 p-2.5 rounded-lg shadow-sm cursor-pointer hover:border-blue-400 hover:shadow-md transition-all group"
-                    >
-                      <div className="flex justify-between items-start">
-                        <div className="flex items-center gap-1.5 font-bold text-slate-700 text-sm group-hover:text-blue-600 transition-colors">
-                          <User size={12} className="text-blue-500" />
-                          <span className="truncate max-w-[100px]">
-                            {task.user_inputs?.first_name} {task.user_inputs?.last_name}
-                          </span>
-                        </div>
-                        <span className="text-[10px] font-mono text-slate-400">#{task.id}</span>
-                      </div>
-                      
-                      <div className="flex items-center gap-1.5 mt-1 text-xs text-slate-500">
-                        <Mail size={10} />
-                        <span className="truncate max-w-[140px]">{task.user_inputs?.email}</span>
-                      </div>
-
-                      {isHighRisk && (
-                        <div className="mt-1.5 pt-1.5 border-t border-slate-50 flex items-center gap-1 text-[10px] text-red-600 font-medium">
-                          <AlertTriangle size={10} />
-                          <span>风险预警: 易取消</span>
-                        </div>
-                      )}
-                    </div>
-                  ))
-                )}
-              </div>
-            </div>
-
-          </div>
-        );
-      })}
-    </div>
-  );
-}

+ 0 - 202
src/components/admin/slots/DashboardTaskPopup.tsx

@@ -1,202 +0,0 @@
-'use client';
-
-import { useState, useEffect, useRef, useLayoutEffect } from 'react';
-import { 
-  X, CheckCircle, RotateCcw, Loader2, 
-  User, History, Mail, Clock, Hash, MessageCircle // 1. 引入 MessageCircle
-} from 'lucide-react';
-import api from '@/lib/api';
-import LocalTime from '@/components/common/LocalTime';
-
-interface TaskDetail extends Record<string, any> {
-  id: number;
-  order_id: string;
-  status?: string;
-  grabbed_history?: any;
-  user_inputs?: any;
-  created_at?: string;
-}
-
-interface DashboardTaskPopupProps {
-  isOpen: boolean;
-  onClose: () => void;
-  task: TaskDetail | null;
-  anchorEl: HTMLElement | null;
-  onSuccess: () => void;
-}
-
-export default function DashboardTaskPopup({ isOpen, onClose, task, anchorEl, onSuccess }: DashboardTaskPopupProps) {
-  const [loading, setLoading] = useState(false);
-  const [style, setStyle] = useState<React.CSSProperties>({ display: 'none' });
-  const popupRef = useRef<HTMLDivElement>(null);
-
-  // 坐标计算逻辑 (保持不变)
-  useLayoutEffect(() => {
-    if (!isOpen || !anchorEl || !popupRef.current) return;
-
-    const calculatePosition = () => {
-      const anchorRect = anchorEl.getBoundingClientRect();
-      const popupRect = popupRef.current!.getBoundingClientRect();
-      const viewportWidth = window.innerWidth;
-      const viewportHeight = window.innerHeight;
-
-      let top = anchorRect.top - popupRect.height - 12; 
-      let left = anchorRect.left + (anchorRect.width / 2) - (popupRect.width / 2);
-
-      if (top < 10) top = anchorRect.bottom + 12;
-      if (top + popupRect.height > viewportHeight) top = viewportHeight - popupRect.height - 10;
-      if (left < 10) left = 10;
-      if (left + popupRect.width > viewportWidth - 10) left = viewportWidth - popupRect.width - 10;
-
-      setStyle({
-        top: `${top}px`,
-        left: `${left}px`,
-        position: 'fixed',
-        zIndex: 60,
-        opacity: 1,
-        transform: 'scale(1)',
-      });
-    };
-
-    calculatePosition();
-    window.addEventListener('resize', calculatePosition);
-    window.addEventListener('scroll', calculatePosition, true);
-
-    return () => {
-      window.removeEventListener('resize', calculatePosition);
-      window.removeEventListener('scroll', calculatePosition, true);
-    };
-  }, [isOpen, anchorEl, task]);
-
-  // 点击外部关闭逻辑 (保持不变)
-  useEffect(() => {
-    const handleClickOutside = (event: MouseEvent) => {
-      if (anchorEl && anchorEl.contains(event.target as Node)) return;
-      if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
-        onClose();
-      }
-    };
-    if (isOpen) document.addEventListener('mousedown', handleClickOutside);
-    return () => document.removeEventListener('mousedown', handleClickOutside);
-  }, [isOpen, onClose, anchorEl]);
-
-  const handleAction = async (action: 'confirm' | 'retry') => {
-    if (!confirm(action === 'confirm' ? "确认标记完成?" : "确认重回队列?")) return;
-
-    setLoading(true);
-    try {
-      const endpoint = action === 'confirm' 
-        ? '/api/vas/task/manual_confirm' 
-        : '/api/vas/task/return_to_queue';
-        
-      await api.post(endpoint, null, { params: { task_id: task?.id } });
-      onSuccess(); 
-      onClose();   
-    } catch (e: any) {
-      alert("操作失败: " + e.message);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  if (!isOpen || !task) return null;
-
-  // 数据解析
-  const userName = task.user_inputs?.first_name 
-    ? `${task.user_inputs.first_name} ${task.user_inputs.last_name}` 
-    : 'Unknown User';
-  const email = task.user_inputs?.email || '-';
-  // 2. 获取社交账号
-  const socialAccount = task.user_inputs?.social_media_account; 
-  
-  const grabbedInfo = task.grabbed_history?.book_date 
-    ? new Date(task.grabbed_history.book_date).toLocaleString() 
-    : '暂无时间';
-
-  return (
-    <div 
-      ref={popupRef}
-      style={style}
-      className="w-72 bg-white/95 backdrop-blur-xl border border-white/50 rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.15)] transition-all duration-200 ease-out origin-center flex flex-col overflow-hidden"
-    >
-      {/* Header */}
-      <div className="px-5 py-3 border-b border-slate-200/60 flex justify-between items-center bg-white/50">
-        <div className="flex items-center gap-2">
-          <span className="bg-slate-900 text-white text-[10px] font-mono px-1.5 py-0.5 rounded">#{task.id}</span>
-          <span className={`text-xs font-bold uppercase ${task.status === 'completed' ? 'text-green-600' : 'text-slate-700'}`}>
-            {task.status}
-          </span>
-        </div>
-        <button onClick={onClose} className="text-slate-400 hover:text-slate-700 transition">
-          <X size={16} />
-        </button>
-      </div>
-
-      {/* Info Body */}
-      <div className="p-5 space-y-4">
-        
-        {/* User Info Section */}
-        <div className="flex items-start gap-3">
-          <div className="p-2 bg-blue-50 text-blue-600 rounded-lg shrink-0 mt-0.5">
-            <User size={16} />
-          </div>
-          <div className="min-w-0 flex-1">
-            <p className="text-sm font-bold text-slate-800 truncate" title={userName}>{userName}</p>
-            
-            {/* Email */}
-            <div className="flex items-center gap-1 text-xs text-slate-500 mt-1">
-              <Mail size={10} className="shrink-0" /> <span className="truncate">{email}</span>
-            </div>
-
-            {/* === 3. 新增:社交账号展示 === */}
-            {socialAccount && (
-              <div className="flex items-center gap-1.5 text-xs font-medium text-indigo-700 bg-indigo-50 border border-indigo-100 px-2 py-1 rounded-md mt-2 w-fit max-w-full">
-                <MessageCircle size={10} className="shrink-0" />
-                <span className="truncate" title={socialAccount}>{socialAccount}</span>
-              </div>
-            )}
-          </div>
-        </div>
-
-        {/* Task Info Section */}
-        <div className="bg-slate-50/80 rounded-lg p-3 border border-slate-100 text-xs space-y-2">
-          <div className="flex justify-between items-center">
-              <span className="text-slate-400 flex items-center gap-1"><Hash size={10}/> Order</span>
-              <span className="font-mono text-slate-600 truncate max-w-[120px]">{task.order_id}</span>
-          </div>
-          <div className="flex justify-between items-center">
-              <span className="text-slate-400 flex items-center gap-1"><History size={10}/> Grabbed</span>
-              <span className="font-medium text-green-700 truncate max-w-[140px]">{grabbedInfo}</span>
-          </div>
-          <div className="flex justify-between items-center">
-              <span className="text-slate-400 flex items-center gap-1"><Clock size={10}/> Created</span>
-              <span className="text-slate-600"><LocalTime date={task.created_at} /></span>
-          </div>
-        </div>
-
-      </div>
-
-      {/* Footer Actions */}
-      <div className="p-3 grid grid-cols-2 gap-3 bg-slate-50/50 border-t border-slate-100">
-        <button 
-          onClick={() => handleAction('retry')}
-          disabled={loading}
-          className="flex items-center justify-center gap-1.5 py-2 rounded-lg border border-slate-200 bg-white text-slate-600 text-xs font-bold hover:bg-slate-50 hover:text-blue-600 transition active:scale-95"
-        >
-          {loading ? <Loader2 size={14} className="animate-spin"/> : <RotateCcw size={14} />}
-          重回队列
-        </button>
-
-        <button 
-          onClick={() => handleAction('confirm')}
-          disabled={loading}
-          className="flex items-center justify-center gap-1.5 py-2 rounded-lg bg-slate-900 text-white text-xs font-bold hover:bg-slate-800 transition shadow-sm active:scale-95"
-        >
-          {loading ? <Loader2 size={14} className="animate-spin"/> : <CheckCircle size={14} />}
-          标记完成
-        </button>
-      </div>
-
-    </div>
-  );
-}

+ 0 - 215
src/components/admin/slots/ProbabilityManager.tsx

@@ -1,215 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { 
-  Percent, Trash2, Plus, Save, RotateCcw, Loader2, X 
-} from 'lucide-react';
-import LocalTime from '@/components/common/LocalTime';
-
-interface ProbItem {
-  prob_key: string; // ISO DateTime
-  prob_val: number; // 0.0 - 1.0
-}
-
-export default function ProbabilityManager() {
-  const [probs, setProbs] = useState<ProbItem[]>([]);
-  const [loading, setLoading] = useState(false);
-  
-  // 新增/编辑表单
-  const [form, setForm] = useState({
-    prob_key: '', // DateTime local string
-    prob_val: 0.5
-  });
-
-  // 重置日期
-  const [resetDate, setResetDate] = useState('');
-
-  useEffect(() => {
-    fetchProbs();
-  }, []);
-
-  const fetchProbs = async () => {
-    setLoading(true);
-    try {
-      // API: GET /api/troov/list-probs
-      const res = await api.get('/api/troov/list-probs');
-      const data = res.data.data;
-      if (Array.isArray(data)) {
-        // 按时间排序
-        data.sort((a: ProbItem, b: ProbItem) => a.prob_key.localeCompare(b.prob_key));
-        setProbs(data);
-      }
-    } catch (e) {
-      console.error("Fetch probs failed", e);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  const handleSetProb = async () => {
-    if (!form.prob_key) return alert("请选择时间");
-    // 转换为 ISO 格式 (后端可能需要完整 ISO,或者 YYYY-MM-DDTHH:mm:ss)
-    // input type="datetime-local" 的值是 YYYY-MM-DDTHH:mm
-    const isoKey = new Date(form.prob_key).toISOString().split('.')[0]; // 去掉毫秒,视后端要求
-
-    try {
-      await api.post('/api/troov/set-prob', {
-        prob_key: isoKey,
-        prob_val: Number(form.prob_val)
-      });
-      fetchProbs();
-      // 不清空 form,方便连续添加
-    } catch (e: any) {
-      alert("设置失败: " + e.message);
-    }
-  };
-
-  const handleDeleteProb = async (key: string) => {
-    if (!confirm("确定删除此概率配置吗?")) return;
-    try {
-      await api.delete('/api/troov/del-prob', {
-        data: { prob_key: key } // axios delete body
-      });
-      fetchProbs();
-    } catch (e: any) {
-      alert("删除失败: " + e.message);
-    }
-  };
-
-  const handleResetProbs = async () => {
-    if (!resetDate) return alert("请选择要重置的日期");
-    if (!confirm(`确定要重置 ${resetDate} 当天的所有概率吗?`)) return;
-    
-    try {
-      await api.post(`/api/troov/reset-probs?date=${resetDate}`);
-      alert("重置成功");
-      fetchProbs();
-    } catch (e: any) {
-      alert("重置失败: " + e.message);
-    }
-  };
-
-  return (
-    <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
-      <div className="px-6 py-4 border-b bg-slate-50 flex justify-between items-center">
-        <h3 className="font-bold text-slate-800 flex items-center gap-2">
-          <Percent size={18} className="text-purple-600"/> 概率干预管理 (Probability Override)
-        </h3>
-        <button onClick={fetchProbs} className="text-sm text-blue-600 hover:underline">刷新列表</button>
-      </div>
-
-      <div className="p-6 grid grid-cols-1 lg:grid-cols-3 gap-8">
-        
-        {/* 左侧:列表展示 */}
-        <div className="lg:col-span-2">
-          <h4 className="text-sm font-bold text-slate-700 mb-3">当前生效规则 ({probs.length})</h4>
-          <div className="border border-slate-200 rounded-lg overflow-hidden max-h-[400px] overflow-y-auto bg-slate-50">
-            {loading ? (
-              <div className="p-8 text-center text-gray-400"><Loader2 className="animate-spin inline mr-2"/> 加载中...</div>
-            ) : probs.length === 0 ? (
-              <div className="p-8 text-center text-gray-400 text-sm">暂无概率配置</div>
-            ) : (
-              <table className="min-w-full text-sm text-left">
-                <thead className="bg-slate-100 border-b border-slate-200 text-xs uppercase text-slate-500 sticky top-0">
-                  <tr>
-                    <th className="px-4 py-2">时间点 (Slot Key)</th>
-                    <th className="px-4 py-2">概率值</th>
-                    <th className="px-4 py-2 text-right">操作</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y divide-slate-200 bg-white">
-                  {probs.map((item) => (
-                    <tr key={item.prob_key} className="hover:bg-slate-50 group">
-                      <td className="px-4 py-2 font-mono text-slate-700">
-                        {item.prob_key.replace('T', ' ')}
-                      </td>
-                      <td className="px-4 py-2 font-bold">
-                        <span className={`${item.prob_val > 0.8 ? 'text-green-600' : item.prob_val < 0.2 ? 'text-red-600' : 'text-blue-600'}`}>
-                          {item.prob_val}
-                        </span>
-                      </td>
-                      <td className="px-4 py-2 text-right">
-                        <button 
-                          onClick={() => handleDeleteProb(item.prob_key)}
-                          className="p-1.5 text-slate-400 hover:text-red-600 rounded hover:bg-red-50 transition"
-                        >
-                          <Trash2 size={16} />
-                        </button>
-                      </td>
-                    </tr>
-                  ))}
-                </tbody>
-              </table>
-            )}
-          </div>
-        </div>
-
-        {/* 右侧:操作面板 */}
-        <div className="space-y-6">
-          
-          {/* 添加/修改 */}
-          <div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
-            <h4 className="text-sm font-bold text-purple-900 mb-4 flex items-center gap-2">
-              <Plus size={16} /> 设置/更新概率
-            </h4>
-            <div className="space-y-3">
-              <div>
-                <label className="text-xs font-bold text-purple-700 block mb-1">时间点</label>
-                <input 
-                  type="datetime-local" 
-                  step="1" // 允许选择秒(如果需要)
-                  className="w-full border border-purple-200 rounded p-2 text-sm focus:ring-2 focus:ring-purple-400 outline-none"
-                  value={form.prob_key}
-                  onChange={e => setForm({...form, prob_key: e.target.value})}
-                />
-              </div>
-              <div>
-                <label className="text-xs font-bold text-purple-700 block mb-1">概率 (0.0 - 1.0)</label>
-                <div className="flex items-center gap-2">
-                  <input 
-                    type="number" step="0.01" min="0" max="1"
-                    className="flex-1 border border-purple-200 rounded p-2 text-sm focus:ring-2 focus:ring-purple-400 outline-none"
-                    value={form.prob_val}
-                    onChange={e => setForm({...form, prob_val: parseFloat(e.target.value)})}
-                  />
-                  <span className="text-xs text-purple-600 w-8 text-right">
-                    {(form.prob_val * 100).toFixed(0)}%
-                  </span>
-                </div>
-              </div>
-              <button 
-                onClick={handleSetProb}
-                className="w-full bg-purple-600 text-white py-2 rounded-lg font-bold hover:bg-purple-700 transition shadow-sm flex items-center justify-center gap-2"
-              >
-                <Save size={16} /> 保存配置
-              </button>
-            </div>
-          </div>
-
-          {/* 批量重置 */}
-          <div className="bg-slate-50 p-4 rounded-xl border border-slate-200">
-            <h4 className="text-sm font-bold text-slate-700 mb-4 flex items-center gap-2">
-              <RotateCcw size={16} /> 按日期重置
-            </h4>
-            <div className="space-y-3">
-              <input 
-                type="date"
-                className="w-full border border-slate-300 rounded p-2 text-sm"
-                value={resetDate}
-                onChange={e => setResetDate(e.target.value)}
-              />
-              <button 
-                onClick={handleResetProbs}
-                className="w-full bg-white border border-slate-300 text-slate-600 py-2 rounded-lg font-medium hover:bg-slate-100 hover:text-red-600 transition"
-              >
-                清除该日所有配置
-              </button>
-            </div>
-          </div>
-
-        </div>
-      </div>
-    </div>
-  );
-}

+ 0 - 207
src/components/admin/slots/TroovBookLimitManager.tsx

@@ -1,207 +0,0 @@
-'use client';
-
-import { useState, useEffect, useMemo } from 'react';
-import api from '@/lib/api';
-import { 
-  Loader2, Save, RefreshCw, Settings2, Plus, Trash2, AlertCircle
-} from 'lucide-react';
-import { toast } from 'react-hot-toast'; // 假设你项目中使用了 react-hot-toast,如果没有可以用 alert 替代
-
-export default function TroovBookLimitManager() {
-  const [limits, setLimits] = useState<Record<string, number>>({});
-  const [loading, setLoading] = useState(false);
-  const [saving, setSaving] = useState(false);
-  
-  // 新增时间点的表单状态
-  const [newTime, setNewTime] = useState('');
-  const [newValue, setNewValue] = useState(1);
-
-  useEffect(() => {
-    fetchConfig();
-  }, []);
-
-  // 获取配置
-  const fetchConfig = async () => {
-    setLoading(true);
-    try {
-      const res = await api.get('/api/dynamic-configurations/key/troov.book.limit');
-      let val = res.data?.data?.config_value;
-      
-      // 兼容后端可能返回 JSON 字符串或直接返回对象的情况
-      if (typeof val === 'string') {
-        try { val = JSON.parse(val); } catch (e) { val = {}; }
-      }
-      setLimits(val || {});
-    } catch (error) {
-      console.error('获取配置失败:', error);
-      toast.error('加载名额配置失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  // 保存配置
-  const handleSave = async () => {
-    setSaving(true);
-    try {
-      // 按照你提供的 curl 示例,PUT 接口需要将 config_value 作为序列化后的 JSON 字符串传入
-      await api.put('/api/dynamic-configurations/key/troov.book.limit', {
-        config_value: JSON.stringify(limits)
-      });
-      toast.success('配置保存成功!');
-      fetchConfig();
-    } catch (error) {
-      console.error('保存配置失败:', error);
-      toast.error('保存配置失败,请重试');
-    } finally {
-      setSaving(false);
-    }
-  };
-
-  // 更新某个时间点的值
-  const handleUpdateLimit = (time: string, value: string) => {
-    const num = parseInt(value, 10);
-    setLimits(prev => ({
-      ...prev,
-      [time]: isNaN(num) ? 0 : Math.max(0, num) // 保证不小于0
-    }));
-  };
-
-  // 删除某个时间点
-  const handleDeleteTime = (time: string) => {
-    if (!confirm(`确定要删除 ${time} 的配置吗?`)) return;
-    const newLimits = { ...limits };
-    delete newLimits[time];
-    setLimits(newLimits);
-  };
-
-  // 新增时间点
-  const handleAddTime = (e: React.FormEvent) => {
-    e.preventDefault();
-    if (!newTime) return;
-    
-    // 简单校验格式 HH:mm
-    if (!/^\d{2}:\d{2}$/.test(newTime)) {
-      return toast.error('时间格式需为 HH:mm,如 09:30');
-    }
-    if (limits[newTime] !== undefined) {
-      return toast.error('该时间点已存在!');
-    }
-
-    setLimits(prev => ({ ...prev, [newTime]: newValue }));
-    setNewTime('');
-    setNewValue(1);
-  };
-
-  // 对时间键进行排序 (09:00, 09:15, 10:00...) 方便展示
-  const sortedTimes = useMemo(() => {
-    return Object.keys(limits).sort((a, b) => {
-      const [hA, mA] = a.split(':').map(Number);
-      const [hB, mB] = b.split(':').map(Number);
-      return (hA * 60 + mA) - (hB * 60 + mB);
-    });
-  }, [limits]);
-
-  return (
-    <div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex flex-col">
-      {/* 头部 */}
-      <div className="p-4 border-b border-slate-200 bg-slate-50 flex flex-wrap gap-4 items-center justify-between">
-        <h2 className="font-bold text-slate-800 flex items-center gap-2">
-          <Settings2 size={18} className="text-teal-600" />
-          Troov 预约名额限制配置
-        </h2>
-        
-        <div className="flex items-center gap-3">
-          <button 
-            onClick={fetchConfig} 
-            disabled={loading}
-            className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-slate-600 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition disabled:opacity-50"
-          >
-            <RefreshCw size={14} className={loading ? 'animate-spin' : ''} /> 刷新
-          </button>
-          
-          <button 
-            onClick={handleSave} 
-            disabled={saving}
-            className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-bold text-white bg-teal-600 rounded-lg shadow-sm hover:bg-teal-700 transition disabled:opacity-50"
-          >
-            {saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
-            保存配置
-          </button>
-        </div>
-      </div>
-
-      {/* 主体内容 */}
-      <div className="p-4 md:p-6 bg-slate-50/30">
-        
-        {loading && Object.keys(limits).length === 0 ? (
-          <div className="py-12 flex justify-center text-slate-400">
-            <Loader2 className="animate-spin" size={24} />
-          </div>
-        ) : (
-          <div className="space-y-6">
-            
-            {/* 警告提示 */}
-            <div className="flex items-start gap-2 bg-amber-50 text-amber-800 p-3 rounded-lg border border-amber-200 text-sm">
-              <AlertCircle size={16} className="mt-0.5 shrink-0" />
-              <p>修改此配置将实时影响 Troov 法签机器人对应时间点的<strong>单日最大预约数</strong>。点击右上角“保存配置”后生效。</p>
-            </div>
-
-            {/* 配置网格 */}
-            <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
-              {sortedTimes.map((time) => (
-                <div key={time} className="flex items-center justify-between bg-white border border-slate-200 p-2 rounded-lg shadow-sm group">
-                  <span className="font-mono font-bold text-slate-700 text-sm pl-1">{time}</span>
-                  <div className="flex items-center gap-1">
-                    <input 
-                      type="number" 
-                      min="0"
-                      className="w-14 text-center text-sm font-bold border border-slate-200 rounded p-1 outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-500"
-                      value={limits[time]}
-                      onChange={(e) => handleUpdateLimit(time, e.target.value)}
-                    />
-                    <button 
-                      onClick={() => handleDeleteTime(time)}
-                      className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded transition opacity-0 group-hover:opacity-100"
-                      title="删除时间点"
-                    >
-                      <Trash2 size={14} />
-                    </button>
-                  </div>
-                </div>
-              ))}
-            </div>
-
-            {/* 快速添加时间点 */}
-            <form onSubmit={handleAddTime} className="flex items-center gap-2 pt-4 border-t border-slate-100">
-              <span className="text-sm font-bold text-slate-500">新增时段:</span>
-              <input 
-                type="time" 
-                required
-                className="border border-slate-300 rounded-md px-2 py-1.5 text-sm font-mono outline-none focus:border-teal-500"
-                value={newTime}
-                onChange={(e) => setNewTime(e.target.value)}
-              />
-              <span className="text-sm font-bold text-slate-500">数量:</span>
-              <input 
-                type="number" 
-                min="1"
-                required
-                className="w-16 border border-slate-300 rounded-md px-2 py-1.5 text-sm outline-none focus:border-teal-500 text-center"
-                value={newValue}
-                onChange={(e) => setNewValue(parseInt(e.target.value) || 1)}
-              />
-              <button 
-                type="submit"
-                className="flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm font-medium rounded-md transition"
-              >
-                <Plus size={14} /> 添加
-              </button>
-            </form>
-            
-          </div>
-        )}
-      </div>
-    </div>
-  );
-}

+ 0 - 161
src/components/admin/slots/TroovSessionManager.tsx

@@ -1,161 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import api from '@/lib/api';
-import { Loader2, RefreshCw, Lock, Search } from 'lucide-react';
-
-interface TroovSession {
-  session_id: string;
-  slot_date: string;
-  slot_time: string;
-  source: string;
-  created_at: string;
-}
-
-export default function TroovSessionManager() {
-  const [sessions, setSessions] = useState<TroovSession[]>([]);
-  const [loading, setLoading] = useState(false);
-
-  // 分页与搜索状态
-  const [keyword, setKeyword] = useState<string>('');
-  const [page, setPage] = useState(1);
-  const [total, setTotal] = useState(0);
-  const size = 100; // 每页 100 条
-
-  useEffect(() => {
-    fetchSessions();
-    // 监听 page 变化,自动触发刷新
-  }, [page]); 
-
-  const fetchSessions = async () => {
-    setLoading(true);
-    try {
-      // API 请求,传入 keyword(如果为空则传 undefined)
-      const res = await api.get('/api/troov-session/list', {
-        params: { 
-          page, 
-          size, 
-          keyword: keyword || undefined 
-        }
-      });
-      
-      const data = res.data?.data || res.data; 
-      const items: TroovSession[] = data.items || [];
-
-      // 直接按数据库返回顺序设置
-      setSessions(items);
-      setTotal(data.total || 0);
-    } catch (error) {
-      console.error('Failed to fetch sessions:', error);
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  // 搜索处理
-  const handleSearch = (e: React.FormEvent) => {
-    e.preventDefault();
-    if (page === 1) {
-      fetchSessions();
-    } else {
-      setPage(1); // 切换回第一页会触发 useEffect
-    }
-  };
-
-  return (
-    <div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex flex-col h-full">
-      {/* Header & Controls */}
-      <div className="p-4 border-b border-slate-200 bg-slate-50 flex flex-wrap gap-4 items-center justify-between">
-        <h2 className="font-bold text-slate-800 flex items-center gap-2">
-          <Lock size={18} className="text-indigo-600" />
-          锁单池管理 (Session Pool)
-        </h2>
-        
-        <div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
-          {/* 关键词搜索框 */}
-          <form onSubmit={handleSearch} className="flex relative flex-1 md:flex-none">
-            <input 
-              type="text" 
-              placeholder="搜索 Session / Source"
-              className="w-full md:w-64 text-sm border border-slate-300 rounded-l-md px-3 py-1.5 outline-none focus:border-indigo-500 focus:z-10"
-              value={keyword}
-              onChange={(e) => setKeyword(e.target.value)}
-            />
-            <button type="submit" className="bg-slate-100 border border-l-0 border-slate-300 rounded-r-md px-3 hover:bg-slate-200 text-slate-600 transition">
-              <Search size={14} />
-            </button>
-          </form>
-
-          {/* 刷新按钮 */}
-          <button 
-            onClick={fetchSessions} 
-            className="p-1.5 text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 rounded transition border border-transparent hover:border-indigo-100"
-            title="刷新"
-          >
-            <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
-          </button>
-        </div>
-      </div>
-
-      {/* Table List */}
-      <div className="overflow-x-auto">
-        <table className="w-full text-left text-sm text-slate-600 border-collapse">
-          <thead className="bg-slate-50 text-slate-500 font-medium border-b border-slate-200">
-            <tr>
-              <th className="px-4 py-3 whitespace-nowrap">Slot 时间</th>
-              <th className="px-4 py-3">Session ID / Source</th>
-              <th className="px-4 py-3">创建时间</th>
-            </tr>
-          </thead>
-          <tbody className="divide-y divide-slate-100">
-            {loading && sessions.length === 0 ? (
-              <tr><td colSpan={3} className="py-8 text-center text-slate-400"><Loader2 className="animate-spin inline mr-2"/> 加载中...</td></tr>
-            ) : sessions.length === 0 ? (
-              <tr><td colSpan={3} className="py-8 text-center text-slate-400">暂无锁单数据</td></tr>
-            ) : (
-              sessions.map((item, index) => (
-                <tr key={`${item.session_id}-${index}`} className="hover:bg-slate-50 transition-colors">
-                  <td className="px-4 py-3 font-bold text-slate-800 whitespace-nowrap">
-                    {item.slot_date} <span className="text-indigo-600">{item.slot_time}</span>
-                  </td>
-                  <td className="px-4 py-3 font-mono text-xs max-w-lg">
-                    {/* 使用 break-all 使太长的 session_id 完整换行显示 */}
-                    <div className="break-all text-slate-800 leading-relaxed" title={item.session_id}>
-                      {item.session_id}
-                    </div>
-                    <div className="text-slate-400 mt-1">{item.source}</div>
-                  </td>
-                  <td className="px-4 py-3 text-xs text-slate-500 whitespace-nowrap">
-                    {new Date(item.created_at).toLocaleString()}
-                  </td>
-                </tr>
-              ))
-            )}
-          </tbody>
-        </table>
-      </div>
-
-      {/* Pagination View */}
-      <div className="p-3 border-t border-slate-200 bg-slate-50 flex justify-between items-center text-xs text-slate-500">
-        <span>共 {total} 条</span>
-        <div className="flex items-center gap-2">
-          <button 
-            disabled={page <= 1} 
-            onClick={() => setPage(p => p - 1)} 
-            className="px-2 py-1 border border-slate-300 rounded bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition"
-          >
-            上一页
-          </button>
-          <span className="px-2 py-1 font-medium">第 {page} 页</span>
-          <button 
-            disabled={page * size >= total} 
-            onClick={() => setPage(p => p + 1)} 
-            className="px-2 py-1 border border-slate-300 rounded bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition"
-          >
-            下一页
-          </button>
-        </div>
-      </div>
-    </div>
-  );
-}

+ 5 - 5
src/components/admin/tasks/ExpiringTaskAlert.tsx

@@ -50,7 +50,7 @@ export default function ExpiringTaskAlert({ onViewDetail, onEdit }: ExpiringTask
   const [emailDraft, setEmailDraft] = useState<EmailDraft>({
     taskId: 0,
     customerName: '',
-    from: 'support@visafly.top',
+    from: 'support@text.skin',
     to: '',
     subject: '',
     body: ''
@@ -84,7 +84,7 @@ Please provide a new expected date range so we can continue the service.`;
     setEmailDraft({
       taskId: item.id,
       customerName: item.customer_name,
-      from: 'support@visafly.top',
+      from: 'support@text.skin',
       to: item.email,
       subject: `Appointment Reschedule Required - Order ${item.order_id}`,
       body: textTemplate
@@ -112,14 +112,14 @@ Please provide a new expected date range so we can continue the service.`;
         
         <p style="margin: 0 0 10px 0;">You can visit our official website for more information:</p>
         <p style="margin: 0;">
-          <a href="https://visafly.top" target="_blank" style="color: #2563eb; text-decoration: underline; font-weight: bold;">
-            https://visafly.top
+          <a href="https://text.skin" target="_blank" style="color: #2563eb; text-decoration: underline; font-weight: bold;">
+            https://text.skin
           </a>
         </p>
         
         <br/>
         <p style="margin: 0 0 5px 0;">Best Regards,</p>
-        <p style="margin: 0; font-weight: bold;">VisaFly Service Team</p>
+        <p style="margin: 0; font-weight: bold;">TextSkin Service Team</p>
       </div>
     `;
   };

+ 3 - 3
src/components/admin/tasks/TaskTable.tsx

@@ -57,7 +57,7 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
   const openMessageModal = (task: VasTask, channel: 'sms' | 'whatsapp' | 'email') => {
     let template = '';
     if (channel === 'whatsapp') {
-      template = 'Visafly: Your appointment status changed. Please review updates and confirm next steps.';
+      template = 'TextSkin: Your appointment status changed. Please review updates and confirm next steps.';
     } else if (channel === 'sms') {
       template = 'Your appointment status changed. Please review updates and confirm next steps.';
     } else {
@@ -124,7 +124,7 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
         }
         const res = await api.post('/api/email/send', {
           send_to: email,
-          subject: 'Visafly Notification',
+          subject: 'TextSkin Notification',
           content: messageText,
         });
         const ok = res.data?.code === 0;
@@ -165,7 +165,7 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
   };
 
   const getSmsSender = (task: VasTask) => {
-    return 'Visafly';
+    return 'TextSkin';
   };
 
   const getRecipientEmail = (task: VasTask) => {

+ 4 - 4
src/components/dashboard/Sidebar.tsx

@@ -31,9 +31,9 @@ export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
   const { t } = useLanguage();
   
   // 2. 这里配置你的联系方式
-  const TELEGRAM_USERNAME = "Visafly Support"; 
+  const TELEGRAM_USERNAME = "TextSkin Support"; 
   const TELEGRAM_NUMBER = "+8617386033451"; 
-  const WHATSAPP_NUMBER = "353892125284"; 
+  const WHATSAPP_NUMBER = "8617386033451"; 
 
   const menuItems: MenuItem[] = [
     { id: 'orders', label: t('sidebar.orders'), icon: FileText },
@@ -79,8 +79,8 @@ export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
             <div className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 shrink-0">
               <Mail size={12} />
             </div>
-            <a href="mailto:support@visafly.top" className="hover:text-blue-600 hover:underline truncate">
-              support@visafly.top
+            <a href="mailto:support@text.skin" className="hover:text-blue-600 hover:underline truncate">
+              support@text.skin
             </a>
           </li>
 

+ 0 - 102
src/components/knowledge/KnowledgeCard.tsx

@@ -1,102 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-import { MapPin, ChevronDown, ChevronUp, Calendar } from 'lucide-react';
-// 1. 引入 Hook
-import { useLanguage } from '@/lib/i18n/LanguageContext';
-import { CardData } from '@/types/card';
-
-
-const getImageUrl = (fidString: string | null) => {
-  if (!fidString) return null;
-  if (fidString.startsWith('http') || fidString.startsWith('data:')) {
-    return fidString;
-  }
-  return `/api/resource/download_file?fid=${encodeURIComponent(fidString)}`;
-};
-
-export default function KnowledgeCard({ data }: { data: CardData }) {
-  // 2. 获取翻译函数和当前语言
-  const { t, lang } = useLanguage();
-  
-  const [expanded, setExpanded] = useState(false);
-  const imageUrl = getImageUrl(data.image);
-
-  // 3. 根据语言格式化日期
-  const formattedDate = new Date(data.created_at).toLocaleDateString(
-    lang === 'zh' ? 'zh-CN' : 'en-US',
-    { year: 'numeric', month: 'short', day: 'numeric' }
-  );
-
-  return (
-    <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden hover:shadow-md transition-all duration-300 flex flex-col">
-      
-      {/* 图片区域 */}
-      {imageUrl && (
-        <div className="h-48 w-full bg-slate-100 relative overflow-hidden group flex-shrink-0">
-          <img 
-            src={imageUrl} 
-            alt={data.title} 
-            className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
-            loading="lazy"
-          />
-          <div className="absolute top-3 left-3 flex gap-2">
-             <span className="bg-black/60 text-white text-xs px-2 py-1 rounded backdrop-blur-sm flex items-center gap-1">
-                <MapPin size={10} /> {data.country}
-             </span>
-             {data.label && (
-               <span className="bg-blue-600/80 text-white text-xs px-2 py-1 rounded backdrop-blur-sm">
-                 {data.label}
-               </span>
-             )}
-          </div>
-        </div>
-      )}
-
-      <div className="p-5 flex flex-col">
-        {/* 标题 */}
-        <h3 className="text-lg font-bold text-slate-900 mb-3 line-clamp-2 leading-tight group-hover:text-blue-600 transition-colors">
-          {data.title}
-        </h3>
-
-        {/* 内容预览 (HTML) */}
-        <div 
-          className={`
-            text-sm text-slate-600 leading-relaxed overflow-hidden transition-all duration-500 ease-in-out
-            ${expanded ? 'max-h-[1000px]' : 'max-h-[80px] line-clamp-3'}
-            
-            /* === 样式修复:Tailwind Prose === */
-            [&_a]:text-blue-600 [&_a]:underline [&_a]:font-medium hover:[&_a]:text-blue-800
-            [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:my-2
-            [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:my-2
-            [&_p]:mb-2 last:[&_p]:mb-0
-            [&_img]:max-w-full [&_img]:rounded-lg [&_img]:my-2
-          `}
-          dangerouslySetInnerHTML={{ __html: data.content }}
-        />
-
-        {/* 底部信息 & 展开按钮 */}
-        <div className="mt-4 pt-4 flex items-center justify-between border-t border-slate-50">
-          <div className="flex items-center text-xs text-slate-400 gap-1">
-            <Calendar size={12} />
-            {formattedDate}
-          </div>
-          
-          <button 
-            onClick={(e) => {
-              e.stopPropagation();
-              setExpanded(!expanded);
-            }}
-            className="text-xs font-medium text-blue-600 hover:text-blue-800 flex items-center gap-1 bg-blue-50 px-2 py-1 rounded transition-colors select-none"
-          >
-            {expanded ? (
-              <>{t('knowledge.collapse')} <ChevronUp size={14} /></>
-            ) : (
-              <>{t('knowledge.read_more')} <ChevronDown size={14} /></>
-            )}
-          </button>
-        </div>
-      </div>
-    </div>
-  );
-}

+ 4 - 4
src/lib/i18n/locales/en.ts

@@ -126,7 +126,7 @@ export const en = {
   home: {
     hero_title_prefix: 'Visa Application, ',
     hero_title_highlight: 'Never Been Easier',
-    hero_subtitle: 'Visafly provides global automated visa processing services. Real-time tracking and expert review ensure your trip is worry-free.',
+    hero_subtitle: 'TextSkin provides global automated visa processing services. Real-time tracking and expert review ensure your trip is worry-free.',
     cta_start: 'Start Application',
     cta_check_slots: 'Check Slots',
     
@@ -239,7 +239,7 @@ export const en = {
     reply_placeholder: 'Type your reply...',
     send_failed: 'Send failed, please try again later',
     upload_tooltip: 'Upload attachment (Coming soon)',
-    urgent_tip: 'For urgent matters, please email support@visafly.top',
+    urgent_tip: 'For urgent matters, please email support@text.skin',
     create_success: 'Ticket submitted successfully! We will process it shortly',
     types: {
       refund: 'Refund Request',
@@ -286,7 +286,7 @@ export const en = {
     full_refund_title: 'Full Refund Eligibility',
     full_refund_desc: 'You may apply for a full refund under the following circumstances:',
     full_refund_item_1: 'You have paid for the order, but our system has not yet started any booking operations (Status is Pending).',
-    full_refund_item_2: 'Duplicate charges caused by Visafly system errors.',
+    full_refund_item_2: 'Duplicate charges caused by TextSkin system errors.',
     full_refund_item_3: 'We failed to successfully book a slot for you within the promised timeframe (usually 30 days, depending on the service).',
     standard_refund_title: "Refund Rules & Processing Fees",
     fee_notice_title: "Important: Payment Gateway Fees",
@@ -397,7 +397,7 @@ export const en = {
   terms: {
     subtitle: 'Visitor Agreement',
     title: 'Terms Of Service',
-    description: 'We strive to operate our platform transparently and securely. By engaging with Visafly web services, you agree to the policies outlined in this section.',
+    description: 'We strive to operate our platform transparently and securely. By engaging with TextSkin web services, you agree to the policies outlined in this section.',
     point_one: 'You are responsible for providing accurate personal and travel information.',
     point_two: 'Payments must be completed through the provided channels; we do not store card data ourselves.',
     point_three: 'Orders are subject to local embassy and provider availability; we reserve the right to refuse or cancel services based on objective constraints.',

+ 4 - 4
src/lib/i18n/locales/zh.ts

@@ -126,7 +126,7 @@ export const zh = {
   home: {
     hero_title_prefix: '签证申请,',
     hero_title_highlight: '从未如此简单',
-    hero_subtitle: 'Visafly 为您提供全球签证自动化处理服务。实时追踪状态,专家级审核,让您的出行无后顾之忧。',
+    hero_subtitle: 'TextSkin 为您提供全球签证自动化处理服务。实时追踪状态,专家级审核,让您的出行无后顾之忧。',
     cta_start: '开始申请',
     cta_check_slots: '查询名额',
     
@@ -239,7 +239,7 @@ export const zh = {
     reply_placeholder: '请输入回复内容...',
     send_failed: '发送失败,请稍后重试',
     upload_tooltip: '上传附件(暂不可用)',
-    urgent_tip: '如需紧急处理,请发送邮件至 support@visafly.top',
+    urgent_tip: '如需紧急处理,请发送邮件至 support@text.skin',
     create_success: '工单提交成功!我们会尽快为您处理',
     types: {
       refund: '退款申请',
@@ -391,10 +391,10 @@ export const zh = {
   terms: {
     subtitle: '访客协议',
     title: '服务条款',
-    description: '我们致力于为每一次服务建立清晰的规则。使用 Visafly 平台即意味着您同意遵守以下条款。',
+    description: '我们致力于为每一次服务建立清晰的规则。使用 TextSkin 平台即意味着您同意遵守以下条款。',
     point_one: '请认真核对并提交您的个人与出行信息,确保其真实有效。',
     point_two: '所有支付行为必须通过授权通道完成,我们不会保存您的银行卡号。',
-    point_three: '订单以当地使馆或合作服务商可用性为准,Visafly 有权在客观原因下调整或取消服务。',
+    point_three: '订单以当地使馆或合作服务商可用性为准,TextSkin 有权在客观原因下调整或取消服务。',
     contact_prompt: '如需进一步说明,请通过以下方式联系'
   },
   contact: {