Files
qiming/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/DigitalEmployeeShell.tsx
2026-06-07 18:21:24 +08:00

139 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react';
import { Bell, Mail, X } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import { RoleProvider } from '@/contexts/RoleContext';
import CommandDeck from './CommandDeck';
import MissionCenter from './MissionCenter';
import SkillLibrary from './SkillLibrary';
import DailyReport from './DailyReport';
import OpsSettings from './OpsSettings';
import type { DigitalNavigationTarget, DigitalTab } from './navigation';
const TABS: { id: DigitalTab; label: string }[] = [
{ id: 'home', label: '首页' },
{ id: 'missions', label: '任务中心' },
{ id: 'skills', label: '技能库' },
{ id: 'reports', label: '日报' },
{ id: 'settings', label: '设置' },
];
const DIGITAL_EMPLOYEE_CLOSE_MESSAGE = 'qimingclaw:digital-close';
function isDigitalTab(value: string | null): value is DigitalTab {
return Boolean(value && TABS.some(tab => tab.id === value));
}
function tabFromPathname(pathname: string): DigitalTab | null {
const parts = pathname.split('/').filter(Boolean);
const last = parts.length > 0 ? parts[parts.length - 1]! : null;
return isDigitalTab(last) ? last : null;
}
function targetFromLocation(pathname: string, search: string): DigitalNavigationTarget {
const params = new URLSearchParams(search);
const queryTab = params.get('tab');
const tab = isDigitalTab(queryTab) ? queryTab : tabFromPathname(pathname) ?? 'home';
return {
tab,
date: params.get('date'),
missionId: params.get('mission'),
taskId: params.get('task'),
skillId: params.get('skill'),
reportSection: params.get('section'),
};
}
function TabContent({
target,
onNavigate,
}: {
target: DigitalNavigationTarget;
onNavigate: (target: DigitalNavigationTarget) => void;
}) {
const tab = target.tab;
switch (tab) {
case 'home': return <CommandDeck onNavigate={onNavigate} />;
case 'missions': return <MissionCenter focusMissionId={target.missionId} focusTaskId={target.taskId} focusDate={target.date} />;
case 'skills': return <SkillLibrary focusSkillId={target.skillId} />;
case 'reports': return <DailyReport focusSection={target.reportSection} />;
case 'settings': return <OpsSettings />;
}
}
function closeDigitalWindow() {
if (window.parent && window.parent !== window) {
const targetOrigin = window.location.origin === 'null' ? '*' : window.location.origin;
window.parent.postMessage({ type: DIGITAL_EMPLOYEE_CLOSE_MESSAGE }, targetOrigin);
return;
}
window.close();
}
export default function DigitalEmployeeShell() {
const location = useLocation();
const navigate = useNavigate();
const [target, setTarget] = useState<DigitalNavigationTarget>(() => targetFromLocation(location.pathname, location.search));
useEffect(() => {
setTarget(targetFromLocation(location.pathname, location.search));
}, [location.pathname, location.search]);
const navigateDigital = useCallback((nextTarget: DigitalNavigationTarget) => {
setTarget(nextTarget);
const params = new URLSearchParams();
if (nextTarget.tab !== 'home') params.set('tab', nextTarget.tab);
if (nextTarget.date) params.set('date', nextTarget.date);
if (nextTarget.missionId) params.set('mission', nextTarget.missionId);
if (nextTarget.taskId) params.set('task', nextTarget.taskId);
if (nextTarget.skillId) params.set('skill', nextTarget.skillId);
if (nextTarget.reportSection) params.set('section', nextTarget.reportSection);
navigate({
pathname: location.pathname,
search: params.toString() ? `?${params.toString()}` : '',
}, { replace: false });
}, [location.pathname, navigate]);
return (
<RoleProvider>
<div className="de-shell">
<header className="de-header">
<div className="de-header-content">
<div className="de-header-left">
<div className="de-header-accent-bar" />
<h1 className="de-header-title"></h1>
</div>
<div className="de-header-center">
{TABS.map(tab => (
<button
key={tab.id}
className={`de-nav-btn ${target.tab === tab.id ? 'active' : ''}`}
onClick={() => navigateDigital({ tab: tab.id })}
>
<span>{tab.label}</span>
</button>
))}
</div>
<div className="de-header-right">
<span className="de-header-welcome"></span>
<span className="de-header-username"></span>
<button type="button" className="de-header-icon" title="消息">
<Mail size={16} />
</button>
<button type="button" className="de-header-icon" title="通知">
<Bell size={16} />
</button>
<button type="button" className="de-header-icon de-header-close" title="关闭" onClick={closeDigitalWindow}>
<X size={17} />
</button>
</div>
</div>
</header>
{/* Content */}
<div className="de-shell-body">
<TabContent target={target} onNavigate={navigateDigital} />
</div>
</div>
</RoleProvider>
);
}