139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
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>
|
||
);
|
||
}
|