290 lines
41 KiB
HTML
290 lines
41 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||
<title>RPA工作站 · 高保真交互原型</title>
|
||
<style>
|
||
:root { --primary:#4f46e5; }
|
||
* { box-sizing: border-box; }
|
||
body { overflow: hidden; }
|
||
.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
.scrollbar-thin::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 999px; }
|
||
.focus-ring:focus-visible { outline: none; box-shadow: 0 0 0 2px #6366f1; }
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen bg-gradient-to-b from-indigo-50 to-slate-100 p-6 text-slate-950">
|
||
<main class="mx-auto flex h-[calc(100vh-48px)] max-w-[1240px] flex-col items-center gap-4 overflow-hidden">
|
||
<header class="flex w-[1180px] shrink-0 items-center justify-between gap-4">
|
||
<div class="flex items-center gap-3">
|
||
<div class="grid h-10 w-10 place-items-center rounded-2xl bg-[var(--primary)] text-white shadow-lg shadow-indigo-500/25">
|
||
<i data-lucide="workflow" class="h-5 w-5"></i>
|
||
</div>
|
||
<div>
|
||
<h1 class="m-0 text-xl font-semibold text-slate-950">RPA工作站 原型</h1>
|
||
<p class="m-0 text-sm text-slate-600">本地优先 AI + RPA 视觉标注与托管执行平台</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex rounded-full border border-slate-200 bg-white p-1 shadow-sm" aria-label="端切换">
|
||
<button type="button" data-platform-tab="windows" class="focus-ring h-10 rounded-full bg-[var(--primary)] px-5 text-sm font-medium text-white">Windows 端</button>
|
||
<button type="button" data-platform-tab="mac" class="focus-ring h-10 rounded-full px-5 text-sm font-medium text-slate-600">Mac 端</button>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="min-h-0 shrink-0" data-platform-panel="windows">
|
||
<div class="flex h-[760px] w-[1180px] flex-col overflow-hidden border border-slate-300 bg-white shadow-2xl">
|
||
<div class="flex h-10 shrink-0 items-center border-b border-slate-200 bg-white pl-3">
|
||
<div class="flex flex-1 items-center gap-2 text-sm font-medium text-slate-700">
|
||
<span class="grid h-5 w-5 place-items-center rounded bg-[var(--primary)] text-white"><i data-lucide="workflow" class="h-3.5 w-3.5"></i></span>
|
||
<span>RPA工作站</span>
|
||
</div>
|
||
<button class="grid h-10 w-12 place-items-center text-slate-600 hover:bg-slate-100" aria-label="最小化">—</button>
|
||
<button class="grid h-10 w-12 place-items-center text-slate-600 hover:bg-slate-100" aria-label="最大化">□</button>
|
||
<button class="grid h-10 w-12 place-items-center text-slate-600 hover:bg-red-500 hover:text-white" aria-label="关闭">×</button>
|
||
</div>
|
||
<div class="relative min-h-0 flex-1 overflow-hidden bg-slate-50" data-app-root data-platform="windows"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="hidden min-h-0 shrink-0" data-platform-panel="mac">
|
||
<div class="flex h-[760px] w-[1180px] flex-col overflow-hidden rounded-[1.75rem] bg-white shadow-2xl shadow-slate-900/20 ring-1 ring-slate-200">
|
||
<div class="flex h-11 shrink-0 items-center border-b border-slate-200 bg-slate-50/95 px-4">
|
||
<div class="flex gap-2">
|
||
<span class="h-3 w-3 rounded-full bg-red-500"></span>
|
||
<span class="h-3 w-3 rounded-full bg-yellow-400"></span>
|
||
<span class="h-3 w-3 rounded-full bg-green-500"></span>
|
||
</div>
|
||
<div class="flex-1 text-center text-sm font-medium text-slate-600">RPA工作站</div>
|
||
<div class="w-16"></div>
|
||
</div>
|
||
<div class="relative min-h-0 flex-1 overflow-hidden bg-slate-50" data-app-root data-platform="mac"></div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<div class="hidden" aria-hidden="true" data-page="工作台" data-page-secondary="自动化任务">PRD coverage marker: 名称、类型、索引状态、引用次数、更新时间、权限模式;时间、员工、应用、知识库、任务状态。</div>
|
||
|
||
<template id="app-template">
|
||
<div class="flex h-full overflow-hidden bg-slate-50 text-slate-950">
|
||
<aside class="flex w-[92px] shrink-0 flex-col items-center gap-2 border-r border-slate-200 bg-slate-950 p-3 text-slate-300" data-nav></aside>
|
||
<section class="relative flex min-w-0 flex-1 flex-col overflow-hidden">
|
||
<header class="flex h-20 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-6" data-header></header>
|
||
<main class="min-h-0 flex-1 overflow-y-auto p-6 scrollbar-thin" data-content></main>
|
||
<div class="pointer-events-none absolute right-5 top-24 z-50 space-y-2" data-toasts></div>
|
||
<div class="absolute inset-0 z-40 hidden" data-overlay-layer></div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
const pages = [
|
||
['工作台','layout-dashboard'], ['标注应用','scan-line'], ['知识库','database'], ['AI 员工','bot'], ['自动化任务','list-checks'], ['设置','settings']
|
||
];
|
||
const pageSummaries = {
|
||
'工作台':'环境、资产、执行状态与核心闭环入口', '标注应用':'管理窗口、节点、流程与 Ctrl+E 标注模式', '知识库':'企业知识资产索引、权限与审计', 'AI 员工':'角色、模型、提示词与任务确认', '自动化任务':'任务状态、托管遮罩、失败重试与审计复盘', '设置':'模型、快捷键、诊断、备份与许可证'
|
||
};
|
||
const prototypeState = {
|
||
page:'工作台', activePlatform:'windows', modal:null, overlay:null, annotationStep:1, activeAppTab:'概览', auditOpen:false,
|
||
rpaHosting:false, hostingStep:0, hostingLogs:[], highRisk:false, toast:[], loadingModel:false,
|
||
configStep:0, auditFilters:{employee:'', status:''}, draftBounds:{x:236,y:164,width:128,height:52}, dragging:false,
|
||
kbs:[
|
||
{name:'财务报销制度', description:'报销制度与审批规则', type:'document', status:'ready', refs:8, updated:'2026-06-25 15:30', permission:'private'},
|
||
{name:'客服话术图片集', description:'截图与话术图片 OCR', type:'image', status:'indexing', refs:3, updated:'2026-06-24 11:10', permission:'selected_agents'},
|
||
{name:'HR 入职流程', description:'入职办理流程文件夹', type:'folder', status:'ready', refs:5, updated:'2026-06-23 09:42', permission:'workspace'},
|
||
{name:'培训视频材料', description:'培训视频关键帧', type:'video', status:'failed', refs:0, updated:'2026-06-22 18:08', permission:'private', error:'OCR 关键帧提取失败'}
|
||
],
|
||
agents:[
|
||
{name:'报销审核员', role:'财务审核', status:'running', kb:1, apps:1, last:'2026-06-26 09:10'},
|
||
{name:'客服录单员', role:'客服录入', status:'idle', kb:1, apps:1, last:'昨天 16:20'},
|
||
{name:'HR 入职助手', role:'人事流程', status:'waiting_user', kb:1, apps:0, last:'等待人工确认'}
|
||
],
|
||
nodes:[
|
||
{name:'提交按钮', type:'button', bounds:'{ x: 520, y: 430, width: 96, height: 36 }', actions:'click, wait', confidence:'0.96'},
|
||
{name:'金额输入框', type:'input', bounds:'{ x: 220, y: 285, width: 180, height: 38 }', actions:'type, read', confidence:'0.93'},
|
||
{name:'报销明细表', type:'table', bounds:'{ x: 160, y: 340, width: 460, height: 180 }', actions:'read, click', confidence:'0.88'},
|
||
{name:'审批菜单', type:'menu', bounds:'{ x: 42, y: 120, width: 140, height: 240 }', actions:'click, hotkey', confidence:'0.91'}
|
||
],
|
||
tasks:[
|
||
{name:'报销单托管演示', type:'RPA 托管', status:'running', employee:'报销审核员', app:'企业报销系统', updated:'09:12', error:'', code:''},
|
||
{name:'财务制度索引', type:'知识库索引', status:'succeeded', employee:'系统', app:'知识库', updated:'昨天', error:'', code:''},
|
||
{name:'提交按钮重试', type:'AI 员工执行', status:'failed', employee:'报销审核员', app:'企业报销系统', updated:'周三', error:'目标按钮置信度低,请重新标注节点', code:'RPA_NODE_CONFIDENCE_LOW'}
|
||
],
|
||
audits:[
|
||
{time:'09:10', employee:'报销审核员', app:'企业报销系统', kb:'财务报销制度', status:'running', text:'RPA 托管开始,动作范围已确认'},
|
||
{time:'09:11', employee:'报销审核员', app:'企业报销系统', kb:'财务报销制度', status:'running', text:'读取报销规则并定位金额输入框'},
|
||
{time:'昨天', employee:'系统', app:'知识库', kb:'财务报销制度', status:'succeeded', text:'知识库索引完成'},
|
||
{time:'周三', employee:'报销审核员', app:'企业报销系统', kb:'财务报销制度', status:'failed', text:'目标按钮置信度低,请重新标注节点'}
|
||
]
|
||
};
|
||
|
||
function clsBadge(kind){
|
||
const map = {ready:'bg-emerald-50 text-emerald-700 ring-emerald-200', indexing:'bg-amber-50 text-amber-700 ring-amber-200', pending:'bg-blue-50 text-blue-700 ring-blue-200', failed:'bg-red-50 text-red-700 ring-red-200', running:'bg-indigo-50 text-indigo-700 ring-indigo-200', idle:'bg-slate-100 text-slate-700 ring-slate-200', waiting_user:'bg-orange-50 text-orange-700 ring-orange-200', succeeded:'bg-emerald-50 text-emerald-700 ring-emerald-200', canceled:'bg-slate-100 text-slate-700 ring-slate-200', queued:'bg-blue-50 text-blue-700 ring-blue-200'};
|
||
return 'inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ring-1 ' + (map[kind] || map.idle);
|
||
}
|
||
function icon(name, c='h-4 w-4'){ return `<i data-lucide="${name}" class="${c}"></i>`; }
|
||
function toast(text, type='success'){ prototypeState.toast.push({text,type,id:Date.now()}); setTimeout(()=>{ prototypeState.toast.shift(); renderAll(); }, 2400); renderAll(); }
|
||
function renderAll(){ document.querySelectorAll('[data-app-root]').forEach(root => renderRoot(root)); if (window.lucide) lucide.createIcons(); }
|
||
function init(){
|
||
const tpl = document.getElementById('app-template');
|
||
document.querySelectorAll('[data-app-root]').forEach(root => root.appendChild(tpl.content.cloneNode(true)));
|
||
renderAll();
|
||
}
|
||
function renderRoot(root){
|
||
root.dataset.appRoot = '';
|
||
const nav = root.querySelector('[data-nav]');
|
||
nav.innerHTML = `<div class="flex h-full w-full flex-col"><div class="flex flex-1 flex-col items-center gap-2">${pages.map(([p,ic]) => { const active = prototypeState.page===p; return `<button type="button" data-page="${p}" aria-label="${p}" aria-current="${active?'page':'false'}" class="focus-ring group flex min-h-[56px] w-full flex-col items-center justify-center gap-1 rounded-2xl text-[11px] transition ${active?'bg-indigo-500 text-white shadow-lg shadow-indigo-500/25':'text-slate-400 hover:bg-white/10 hover:text-indigo-100'}"><span class="${active?'text-white':'text-slate-400 group-hover:text-indigo-200'}">${icon(ic,'h-5 w-5')}</span><span>${p}</span></button>`; }).join('')}</div><div class="mt-2 flex w-full flex-col items-center rounded-2xl bg-slate-900/90 px-2 py-2 text-[10px] leading-4 text-slate-400 ring-1 ring-white/5"><span class="mb-1 text-emerald-400">${icon('circle-dot','h-3.5 w-3.5')}</span><span>本地在线</span></div></div>`;
|
||
const header = root.querySelector('[data-header]');
|
||
header.innerHTML = `<div><h2 class="text-xl font-semibold">${prototypeState.page}</h2><p class="mt-1 text-sm text-slate-500">${pageSummaries[prototypeState.page]}</p></div><div class="flex flex-wrap justify-end gap-2">${chip('模型 在线','activity','emerald')}${chip(prototypeState.rpaHosting?'RPA 托管中':'RPA 空闲','radio','indigo')}${chip('本地模式','hard-drive','slate')}${chip('试用 14天','badge-check','amber')}</div>`;
|
||
root.querySelector('[data-content]').innerHTML = renderPage();
|
||
root.querySelector('[data-toasts]').innerHTML = prototypeState.toast.map(t => `<div class="pointer-events-auto flex min-w-[280px] items-start gap-2 rounded-xl border bg-white px-4 py-3 text-sm shadow-xl ${t.type==='error'?'border-red-200 text-red-700':'border-emerald-200 text-slate-700'}">${icon(t.type==='error'?'circle-alert':'circle-check','mt-0.5 h-4 w-4 shrink-0')}<span>${t.text}</span></div>`).join('');
|
||
renderOverlay(root);
|
||
}
|
||
function chip(text, ic, tone){ return `<span class="inline-flex h-8 items-center gap-1.5 rounded-full bg-${tone==='slate'?'slate':'white'}-${tone==='slate'?'100':'50'} px-3 text-xs font-medium text-${tone}-700 ring-1 ring-${tone}-200">${icon(ic)}${text}</span>`; }
|
||
function card(title, body, footer=''){ return `<article class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-4 flex items-center gap-2 font-semibold text-slate-900">${body.icon||''}${title}</h3>${body.html||body}${footer}</article>`; }
|
||
function actionBtn(label, attrs='', primary=false){ return `<button type="button" ${attrs} class="focus-ring inline-flex h-10 items-center justify-center gap-2 rounded-xl ${primary?'bg-[var(--primary)] text-white shadow-lg shadow-indigo-500/20':'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50'} px-4 text-sm font-medium">${label}</button>`; }
|
||
|
||
function renderPage(){
|
||
if (prototypeState.page==='工作台') return dashboardPage();
|
||
if (prototypeState.page==='知识库') return kbPage();
|
||
if (prototypeState.page==='AI 员工') return agentPage();
|
||
if (prototypeState.page==='标注应用') return annotationPage();
|
||
if (prototypeState.page==='自动化任务') return tasksPage();
|
||
return settingsPage();
|
||
}
|
||
function dashboardPage(){
|
||
return `<div class="space-y-6">
|
||
<div class="grid grid-cols-3 gap-4">
|
||
${card('环境状态',{icon:icon('shield-check'),html:`<div class="space-y-3 text-sm"><div class="flex justify-between"><span>权限</span><b>3/4</b></div><div class="flex justify-between"><span>模型连通</span><span class="text-emerald-600">通过</span></div><div class="flex justify-between"><span>本地 GPU</span><span>中档</span></div><div class="flex justify-between"><span>存储</span><span class="text-emerald-600">可用</span></div>${actionBtn('查看首次配置','data-modal-open="config"')}</div>`})}
|
||
${card('资产状态',{icon:icon('boxes'),html:`<div class="grid grid-cols-2 gap-3 text-sm"><b>知识库 3</b><b>就绪索引 2</b><b>AI 员工 2</b><b>标注应用 1</b></div>`})}
|
||
${card('执行状态',{icon:icon('activity'),html:`<div class="space-y-3 text-sm"><div class="flex justify-between"><span>运行中</span><b>0</b></div><div class="flex justify-between"><span>失败任务</span><b class="text-red-600">1</b></div><div class="flex justify-between"><span>最近托管</span><span class="text-emerald-600">成功</span></div><div class="flex justify-between"><span>RPA</span><span>${prototypeState.rpaHosting?'托管中':'空闲'}</span></div></div>`})}
|
||
</div>
|
||
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-4 font-semibold">快速入口</h3><div class="flex flex-wrap gap-3">
|
||
${actionBtn(icon('database')+'创建知识库','data-modal-open="knowledge"',true)}
|
||
${actionBtn(icon('bot')+'创建 AI 员工','data-modal-open="agent"',true)}
|
||
${actionBtn(icon('scan-line')+'进入标注模式 Ctrl+E','data-overlay-open="annotation"')}
|
||
${actionBtn(icon('play')+'启动自动化任务','data-modal-open="hosting-confirm"')}
|
||
${actionBtn(icon('package-check')+'导出诊断包','data-action="diagnostic"')}
|
||
</div></section>
|
||
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-4 font-semibold">核心闭环</h3><div class="flex items-center gap-2 text-sm text-slate-600">${['首次配置','知识库','AI 员工','标注应用','自动化任务','托管执行','审计复盘'].map((s,i)=>`<div class="flex items-center gap-2"><span class="grid h-8 w-8 place-items-center rounded-full bg-indigo-50 font-semibold text-indigo-700">${i+1}</span><span>${s}</span></div>${i<6?'<div class="h-px w-8 bg-slate-200"></div>':''}`).join('')}</div></section>
|
||
</div>`;
|
||
}
|
||
function kbPage(){
|
||
return `<div class="space-y-5"><div class="flex items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"><div class="relative flex-1">${icon('search','absolute left-3 top-3 h-4 w-4 text-slate-400')}<input class="focus-ring h-10 w-full rounded-xl border border-slate-200 pl-9 text-sm" placeholder="搜索制度、流程、操作说明"></div>${actionBtn('来源','')}${actionBtn('类型','')}${actionBtn('按更新时间','')}${actionBtn(icon('upload')+'上传/创建知识库','data-modal-open="knowledge"',true)}</div>${table(['名称','类型','索引状态','引用次数','更新时间','权限模式'], prototypeState.kbs.map((k,i)=>[`<div><b>${k.name}</b><p class="text-xs text-slate-500">${k.description}${k.error?` · ${k.error} ${actionBtn('重试',`data-action="retry-kb" data-index="${i}"`)}`:''}</p></div>`,k.type,`<span class="${clsBadge(k.status)}">${k.status}</span>`,k.refs,k.updated,`<button data-action="permission" data-index="${i}" class="${clsBadge('idle')}">${k.permission}</button>`]))}</div>`;
|
||
}
|
||
function agentPage(){
|
||
return `<div class="space-y-5"><div class="flex justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"><div><h3 class="font-semibold">AI 员工</h3><p class="text-sm text-slate-500">启动任务前必须展示知识库、应用和停止方式。</p></div>${actionBtn(icon('plus')+'创建 AI 员工','data-modal-open="agent"',true)}</div>${table(['名称','角色','状态','可访问知识库','可操作标注应用','最近执行时间','操作'], prototypeState.agents.map((a,i)=>[a.name,a.role,`<span class="${clsBadge(a.status)}">${a.status}</span>`,a.kb,a.apps,a.last,actionBtn('启动任务',`data-modal-open="agent-confirm" data-index="${i}"`)]))}</div>`;
|
||
}
|
||
function annotationPage(){
|
||
const tab = prototypeState.activeAppTab;
|
||
const tabHtml = ['概览','窗口','节点','流程'].map(t=>`<button type="button" data-tab="${t}" class="focus-ring h-10 rounded-xl px-4 text-sm font-medium ${tab===t?'bg-indigo-600 text-white':'text-slate-600 hover:bg-slate-100'}">${t}</button>`).join('');
|
||
let panel = tab==='节点' ? table(['节点名','node_type','bounds','allowed_actions','confidence'], prototypeState.nodes.map(n=>[n.name,n.type,n.bounds,n.actions,n.confidence])) : tab==='窗口' ? `<div class="grid grid-cols-2 gap-4 text-sm"><div class="rounded-xl border p-4"><b>主窗口</b><p>1180×760 · 已标注</p></div><div class="rounded-xl border p-4"><b>审批弹窗</b><p>520×360 · 已标注</p></div></div>` : tab==='流程' ? `<ol class="space-y-3 text-sm"><li>1. 读取制度规则</li><li>2. 填写金额输入框</li><li>3. 点击提交按钮前暂停确认</li></ol>` : `<div class="grid grid-cols-4 gap-4 text-sm"><div class="rounded-xl border p-4"><b>窗口数</b><p>2</p></div><div class="rounded-xl border p-4"><b>节点数</b><p>${prototypeState.nodes.length}</p></div><div class="rounded-xl border p-4"><b>流程数</b><p>3</p></div><div class="rounded-xl border p-4"><b>最近更新</b><p>今天 09:20</p></div></div>`;
|
||
return `<div class="space-y-5"><div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><div class="flex items-center justify-between"><div><h3 class="font-semibold">企业报销系统</h3><p class="text-sm text-slate-500">窗口数 2、节点数 ${prototypeState.nodes.length}、流程数 3、最近更新 今天</p></div>${actionBtn(icon('scan-line')+'进入标注模式 Ctrl+E','data-overlay-open="annotation"',true)}</div></div><section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><div class="mb-5 flex gap-2">${tabHtml}</div>${panel}</section></div>`;
|
||
}
|
||
function tasksPage(){
|
||
const rows = prototypeState.tasks.map((t,i)=>[t.name,t.type,`<span class="${clsBadge(t.status)}">${t.status}</span>`,t.employee,t.app,t.updated,t.error?`<div class="text-red-700">${t.error}<br><code class="text-xs">${t.code}</code><br>${actionBtn('重试',`data-action="retry-task" data-index="${i}"`)}</div>`:actionBtn('查看','')]);
|
||
let audits = prototypeState.audits.filter(a=>(!prototypeState.auditFilters.employee||a.employee===prototypeState.auditFilters.employee)&&(!prototypeState.auditFilters.status||a.status===prototypeState.auditFilters.status));
|
||
return `<div class="space-y-5"><div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"><div><h3 class="font-semibold">自动化任务</h3><p class="text-sm text-slate-500">状态只使用 queued | running | succeeded | failed | canceled。</p></div>${actionBtn(icon('play')+'启动托管演示','data-modal-open="hosting-confirm"',true)}</div><div class="grid grid-cols-3 gap-4">${['运行中任务','历史任务','失败任务'].map((s,i)=>`<div class="rounded-2xl border bg-white p-4 shadow-sm"><h3 class="font-semibold">${s}</h3><p class="mt-2 text-2xl font-semibold">${i===0?prototypeState.tasks.filter(t=>t.status==='running').length:i===1?prototypeState.tasks.filter(t=>t.status==='succeeded').length:prototypeState.tasks.filter(t=>t.status==='failed').length}</p></div>`).join('')}</div>${table(['任务名','类型','状态','员工','目标应用','更新时间','操作'], rows)}<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><button type="button" data-action="toggle-audit" class="flex h-10 w-full items-center justify-between font-semibold">审计日志 ${icon(prototypeState.auditOpen?'chevron-up':'chevron-down')}</button><div class="${prototypeState.auditOpen?'':'hidden'} mt-4 space-y-4"><div class="grid grid-cols-5 gap-3 text-sm"><select data-filter="time" class="h-10 rounded-xl border px-3"><option>时间</option></select><select data-filter="employee" class="h-10 rounded-xl border px-3"><option value="">员工</option><option>报销审核员</option><option>系统</option></select><select data-filter="app" class="h-10 rounded-xl border px-3"><option>应用</option></select><select data-filter="kb" class="h-10 rounded-xl border px-3"><option>知识库</option></select><select data-filter="status" class="h-10 rounded-xl border px-3"><option value="">任务状态</option><option>running</option><option>succeeded</option><option>failed</option><option>canceled</option></select></div>${table(['时间','员工','应用','知识库','状态','记录'], audits.map(a=>[a.time,a.employee,a.app,a.kb,`<span class="${clsBadge(a.status)}">${a.status}</span>`,a.text]))}</div></section></div>`;
|
||
}
|
||
function settingsPage(){
|
||
const groups = [
|
||
['模型配置',`本地模型 Qwen2.5 · 云端模型 可选 · API Key sk-•••••••• ${actionBtn(prototypeState.loadingModel?'测试中...':'连通性测试','data-action="model-test"')}`],
|
||
['提示词配置','系统提示词模板、员工提示词、任务提示词'], ['存储位置','SQLite 数据库、知识库原文、索引文件、日志目录'],
|
||
['快捷键管理','Ctrl+E 标注模式 · ESC 紧急停止 · 悬浮窗显示/隐藏'], ['开关配置','本地/云端模式 · 托管确认 已开启 · 高风险动作二次确认 已开启'],
|
||
['诊断与日志',`${actionBtn('导出诊断包','data-action="diagnostic-full"')} ${actionBtn('查看本地日志目录','data-action="log-dir"')} ${actionBtn('复制环境信息','data-action="copy-env"')}`],
|
||
['数据备份与恢复','导出配置、导出知识库元数据、恢复 SQLite 与索引文件'], ['许可证管理','trial · 14 天试用 · 最多 3 个知识库 / 2 个 AI 员工 / 1 个标注应用 / 每日 20 次托管任务']
|
||
];
|
||
return `<div class="grid grid-cols-2 gap-4 pb-8">${groups.map(g=>`<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"><h3 class="mb-3 font-semibold">${g[0]}</h3><div class="text-sm leading-7 text-slate-600">${g[1]}</div></section>`).join('')}</div>`;
|
||
}
|
||
function table(headers, rows){ return `<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"><table class="w-full text-left text-sm"><thead class="bg-slate-50 text-xs text-slate-500"><tr>${headers.map(h=>`<th class="px-4 py-3 font-medium">${h}</th>`).join('')}</tr></thead><tbody class="divide-y divide-slate-100">${rows.map(r=>`<tr class="align-top hover:bg-slate-50">${r.map(c=>`<td class="px-4 py-3">${c}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`; }
|
||
|
||
function renderOverlay(root){
|
||
const layer = root.querySelector('[data-overlay-layer]');
|
||
layer.className = 'absolute inset-0 z-40 ' + ((prototypeState.modal || prototypeState.overlay || prototypeState.rpaHosting || prototypeState.highRisk) ? '' : 'hidden');
|
||
if (prototypeState.rpaHosting) { layer.innerHTML = hostingOverlay(); return; }
|
||
if (prototypeState.highRisk) { layer.innerHTML = highRiskOverlay(); return; }
|
||
if (prototypeState.overlay==='annotation') { layer.innerHTML = annotationOverlay(); return; }
|
||
if (prototypeState.modal) { layer.innerHTML = modalHtml(prototypeState.modal); return; }
|
||
layer.innerHTML = '';
|
||
}
|
||
function modalFrame(title, body, wide=false){ return `<div class="flex h-full items-center justify-center bg-slate-950/40 p-8"><div class="max-h-full ${wide?'w-[760px]':'w-[560px]'} overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl"><div class="mb-5 flex items-center justify-between"><h3 class="text-lg font-semibold">${title}</h3><button data-modal-close class="focus-ring grid h-9 w-9 place-items-center rounded-xl hover:bg-slate-100">${icon('x')}</button></div>${body}</div></div>`; }
|
||
function modalHtml(type){
|
||
if(type==='config') return configModal();
|
||
if(type==='knowledge') return modalFrame('上传/创建知识库', `<form data-form="knowledge" class="space-y-4"><input name="name" required class="focus-ring h-10 w-full rounded-xl border px-3" placeholder="名称"><textarea name="description" class="focus-ring w-full rounded-xl border p-3" placeholder="描述"></textarea><select name="type" class="h-10 w-full rounded-xl border px-3"><option value="document">document</option><option value="image">image</option><option value="video">video</option><option value="folder">folder</option><option value="url">url</option></select><select name="permission" class="h-10 w-full rounded-xl border px-3"><option value="private">private</option><option value="selected_agents">selected_agents</option><option value="workspace">workspace</option></select><button class="h-10 rounded-xl bg-indigo-600 px-4 text-white">提交</button></form>`);
|
||
if(type==='agent') return modalFrame('创建/编辑 AI 员工', `<form data-form="agent" class="space-y-4"><input name="role" required class="focus-ring h-10 w-full rounded-xl border px-3" placeholder="角色描述"><textarea name="prompt" class="focus-ring w-full rounded-xl border p-3" placeholder="系统提示词"></textarea><input class="h-10 w-full rounded-xl border px-3" placeholder="模型配置:本地模型"><input class="h-10 w-full rounded-xl border px-3" placeholder="知识库权限:财务报销制度"><input class="h-10 w-full rounded-xl border px-3" placeholder="可操作应用权限:企业报销系统"><button class="h-10 rounded-xl bg-indigo-600 px-4 text-white">提交</button></form>`);
|
||
if(type==='permission') return modalFrame('权限模式', `<div class="space-y-3">${['private','selected_agents','workspace'].map(p=>`<button data-action="save-permission" data-value="${p}" class="focus-ring flex h-10 w-full items-center rounded-xl border px-3 text-sm hover:bg-slate-50">${p}</button>`).join('')}</div>`);
|
||
if(type==='agent-confirm' || type==='hosting-confirm') return modalFrame('启动前确认', `<div class="space-y-4 text-sm"><div class="rounded-xl bg-indigo-50 p-4"><b>AI 员工</b><p>报销审核员</p></div><div class="rounded-xl bg-slate-50 p-4"><b>将访问的知识库</b><p>财务报销制度</p></div><div class="rounded-xl bg-slate-50 p-4"><b>将控制的应用</b><p>企业报销系统</p></div><div class="rounded-xl bg-slate-50 p-4"><b>预计动作范围</b><p>读取报销规则、填写金额、点击提交前暂停确认</p></div><div class="rounded-xl bg-slate-50 p-4"><b>停止方式</b><p>ESC 或遮罩上的停止托管按钮</p></div><div class="flex justify-end gap-3">${actionBtn('取消','data-modal-close')}${actionBtn('确认启动托管','data-action="start-hosting"',true)}</div></div>`);
|
||
return '';
|
||
}
|
||
function configModal(){
|
||
const steps=['权限检查','模型配置','性能评估','存储位置','安全说明']; const s=prototypeState.configStep;
|
||
const status=['通过','通过','通过','通过','需手动处理'];
|
||
return modalFrame('首次配置与环境检查', `<div class="flex gap-5"><aside class="w-40 space-y-2">${steps.map((x,i)=>`<button data-action="config-step" data-index="${i}" class="h-10 w-full rounded-xl text-left text-sm ${s===i?'bg-indigo-600 px-3 text-white':'px-3 hover:bg-slate-100'}">${x}</button>`).join('')}</aside><div class="flex-1 space-y-4 text-sm"><p class="text-slate-500">状态枚举:未检查 | 检查中 | 通过 | 失败 | 需手动处理</p>${s===0?`<div class="space-y-2">${['屏幕录制','辅助功能/自动化控制','文件访问','网络访问'].map((x,i)=>`<div class="flex justify-between rounded-xl border p-3"><span>${x}</span><span class="${clsBadge(i===1?status[4]:'ready')}">${i===1?status[4]:'通过'}</span></div>`).join('')}</div><div class="rounded-xl bg-amber-50 p-4 text-amber-800">原因:辅助功能权限需手动授权;恢复:打开系统设置或重新检测。</div><div class="flex gap-2">${actionBtn('打开系统设置','data-action="open-settings"')}${actionBtn('重新检测','data-action="recheck-permission"',true)}${actionBtn('跳过并以后配置','data-modal-close')}</div>`:`<div class="rounded-xl border p-4"><b>${steps[s]}</b><p class="mt-2">当前状态:通过。本页用于演示 ${steps[s]} 配置检查。</p></div>`}</div></div>`, true);
|
||
}
|
||
function annotationOverlay(){
|
||
const b=prototypeState.draftBounds;
|
||
return `<div class="relative h-full bg-slate-950/85 p-8 text-white"><div class="mx-auto flex h-full max-w-4xl flex-col gap-4"><div class="flex items-center justify-between"><div><h3 class="text-lg font-semibold">Ctrl+E 标注模式</h3><p class="text-sm text-slate-300">选择当前标注应用 → 框选窗口/节点 → 命名节点 → 保存</p></div>${actionBtn('退出','data-overlay-close')}</div><div class="grid grid-cols-[1fr_280px] gap-4 min-h-0 flex-1"><div class="relative overflow-hidden rounded-2xl bg-slate-200 p-5 text-slate-900"><div class="h-full rounded-xl bg-white p-5 shadow-inner"><div class="mb-4 flex h-10 items-center gap-2 rounded bg-slate-100 px-3"><b>企业报销系统</b><span class="text-sm text-slate-500">模拟桌面截图</span></div><div class="grid grid-cols-3 gap-4"><div class="h-80 rounded bg-slate-100"></div><div class="col-span-2 h-80 rounded bg-slate-50 p-4"><input class="mb-4 h-10 w-52 rounded border px-3" value="1280.00"><button class="absolute rounded bg-indigo-600 px-4 py-2 text-white" style="left:520px;top:430px">提交</button></div></div></div><div data-drag-box class="absolute cursor-move rounded-lg border-2 border-indigo-500 bg-indigo-500/20" style="left:${b.x}px;top:${b.y}px;width:${b.width}px;height:${b.height}px"></div></div><form data-form="node" class="space-y-4 rounded-2xl bg-white p-5 text-slate-900"><label class="block text-sm font-medium">当前标注应用<select class="mt-2 h-10 w-full rounded-xl border px-3"><option>企业报销系统</option></select></label><label class="block text-sm font-medium">节点名<input name="name" required class="mt-2 h-10 w-full rounded-xl border px-3" placeholder="测试提交按钮"></label><p class="rounded-xl bg-slate-50 p-3 text-xs">当前 bounds:{ x: ${b.x}, y: ${b.y}, width: ${b.width}, height: ${b.height} }</p><button class="h-10 w-full rounded-xl bg-indigo-600 text-white">保存</button><button type="button" data-action="annotation-fail" class="h-9 text-sm text-red-600">模拟保存失败</button><button type="button" data-action="annotation-retry" class="hidden h-9 text-sm text-indigo-600">重试保存</button><p data-annotation-error class="hidden rounded-xl bg-red-50 p-3 text-sm text-red-700">保存失败:窗口句柄已变化,可保留草稿并重试。原因:窗口句柄已变化;恢复:保留草稿并重试保存。</p></form></div></div></div>`;
|
||
}
|
||
function hostingOverlay(){
|
||
const actions=['读取报销规则','填写金额输入框','定位提交按钮','提交报销单'];
|
||
const current=actions[Math.min(prototypeState.hostingStep, actions.length-1)];
|
||
return `<div class="flex h-full items-center justify-center bg-slate-950/90 p-8 text-white"><div class="w-[760px] rounded-3xl border border-white/10 bg-slate-900 p-6 shadow-2xl"><div class="flex items-center justify-between"><div><h3 class="text-xl font-semibold">RPA 托管执行中</h3><p class="text-sm text-slate-300">ESC 紧急停止始终可用</p></div><button data-action="stop-hosting" class="h-11 rounded-xl bg-red-600 px-5 font-semibold text-white">停止托管</button></div><div class="mt-5 rounded-2xl bg-slate-950 p-4"><p class="text-sm text-slate-400">当前动作</p><p class="mt-1 text-lg font-semibold">${current}</p><div class="mt-4 h-3 rounded-full bg-slate-800"><div class="h-3 rounded-full bg-indigo-500" style="width:${(prototypeState.hostingStep+1)*25}%"></div></div></div><div class="mt-5 max-h-48 overflow-y-auto rounded-2xl bg-slate-950 p-4 text-sm text-slate-300">${prototypeState.hostingLogs.map(l=>`<p>${l}</p>`).join('') || '<p>等待下一步动作。</p>'}</div><div class="mt-5 flex justify-end gap-3">${actionBtn('下一步动作','data-action="next-hosting"',true)}</div></div></div>`;
|
||
}
|
||
function highRiskOverlay(){ return `<div class="flex h-full items-center justify-center bg-slate-950/70 p-8"><div class="w-[520px] rounded-2xl bg-white p-6 shadow-2xl"><h3 class="text-lg font-semibold text-red-700">高风险动作:提交</h3><p class="mt-3 text-sm text-slate-600">确认后将向业务系统提交单据。</p><div class="mt-5 flex justify-end gap-3">${actionBtn('取消并等待人工','data-action="cancel-high-risk"')}${actionBtn('确认提交','data-action="confirm-high-risk"',true)}</div></div></div>`; }
|
||
|
||
document.addEventListener('click', event => {
|
||
const platformTab = event.target.closest('[data-platform-tab]');
|
||
if(platformTab){ const target=platformTab.dataset.platformTab; prototypeState.activePlatform=target; document.querySelectorAll('[data-platform-panel]').forEach(p=>p.classList.toggle('hidden',p.dataset.platformPanel!==target)); document.querySelectorAll('[data-platform-tab]').forEach(b=>{b.className='focus-ring h-10 rounded-full px-5 text-sm font-medium text-slate-600';}); platformTab.className='focus-ring h-10 rounded-full bg-[var(--primary)] px-5 text-sm font-medium text-white'; return; }
|
||
const page = event.target.closest('[data-page]'); if(page){ prototypeState.page=page.dataset.page; renderAll(); return; }
|
||
const tab = event.target.closest('[data-tab]'); if(tab){ prototypeState.activeAppTab=tab.dataset.tab; renderAll(); return; }
|
||
const open = event.target.closest('[data-modal-open]'); if(open){ prototypeState.modal=open.dataset.modalOpen; renderAll(); return; }
|
||
if(event.target.closest('[data-modal-close]')){ prototypeState.modal=null; renderAll(); return; }
|
||
const overlayOpen = event.target.closest('[data-overlay-open]'); if(overlayOpen){ prototypeState.overlay=overlayOpen.dataset.overlayOpen; renderAll(); return; }
|
||
if(event.target.closest('[data-overlay-close]')){ prototypeState.overlay=null; renderAll(); return; }
|
||
const action = event.target.closest('[data-action]'); if(!action) return;
|
||
handleAction(action, event);
|
||
});
|
||
document.addEventListener('change', event => { const f=event.target.closest('[data-filter]'); if(!f) return; prototypeState.auditFilters[f.dataset.filter]=f.value; renderAll(); });
|
||
document.addEventListener('submit', event => {
|
||
event.preventDefault(); const form=event.target;
|
||
if(form.dataset.form==='knowledge'){ const fd=new FormData(form); prototypeState.kbs.unshift({name:fd.get('name')||'测试制度库',description:fd.get('description')||'新建知识库',type:fd.get('type'),status:'pending',refs:0,updated:'刚刚',permission:fd.get('permission')}); prototypeState.page='知识库'; prototypeState.modal=null; toast('知识库已创建,正在进入索引队列'); }
|
||
if(form.dataset.form==='agent'){ const fd=new FormData(form); prototypeState.agents.unshift({name:fd.get('role')||'新 AI 员工',role:fd.get('role')||'业务助理',status:'idle',kb:1,apps:1,last:'未执行'}); prototypeState.page='AI 员工'; prototypeState.modal=null; toast('AI 员工已创建,默认未启动任务'); }
|
||
if(form.dataset.form==='node'){ const fd=new FormData(form); const b=prototypeState.draftBounds; prototypeState.nodes.unshift({name:fd.get('name')||'新建节点',type:'button',bounds:`{ x: ${b.x}, y: ${b.y}, width: ${b.width}, height: ${b.height} }`,actions:'click, wait',confidence:'0.89'}); prototypeState.page='标注应用'; prototypeState.activeAppTab='节点'; prototypeState.overlay=null; toast('节点已保存到 企业报销系统'); }
|
||
renderAll();
|
||
});
|
||
function handleAction(el){
|
||
const a=el.dataset.action;
|
||
if(a==='diagnostic') toast('诊断包已生成,已排除 API Key 与知识库原文');
|
||
if(a==='diagnostic-full') toast('诊断包已生成,不包含 API Key、知识库原文、用户隐私内容');
|
||
if(a==='log-dir') toast('已打开本地日志目录(原型模拟)');
|
||
if(a==='copy-env') toast('环境信息已复制(原型模拟)');
|
||
if(a==='retry-kb'){ prototypeState.kbs[+el.dataset.index].status='indexing'; delete prototypeState.kbs[+el.dataset.index].error; toast('已重新加入索引队列'); }
|
||
if(a==='permission'){ prototypeState.modal='permission'; prototypeState.permissionIndex=+el.dataset.index; }
|
||
if(a==='save-permission'){ prototypeState.kbs[prototypeState.permissionIndex].permission=el.dataset.value; prototypeState.modal=null; prototypeState.audits.unshift({time:'刚刚',employee:'系统',app:'知识库',kb:prototypeState.kbs[prototypeState.permissionIndex].name,status:'succeeded',text:'权限变更已写入审计日志'}); toast('权限变更已写入审计日志'); }
|
||
if(a==='start-hosting') startHosting();
|
||
if(a==='next-hosting') nextHosting();
|
||
if(a==='stop-hosting') stopHosting();
|
||
if(a==='cancel-high-risk'){ prototypeState.highRisk=false; prototypeState.rpaHosting=true; prototypeState.agents.find(x=>x.name==='报销审核员').status='waiting_user'; prototypeState.hostingLogs.push('人工确认被取消,任务等待人工处理。'); prototypeState.audits.unshift({time:'刚刚',employee:'报销审核员',app:'企业报销系统',kb:'财务报销制度',status:'running',text:'人工确认被取消'}); }
|
||
if(a==='confirm-high-risk'){ prototypeState.highRisk=false; prototypeState.rpaHosting=false; prototypeState.tasks[0].status='succeeded'; prototypeState.agents.find(x=>x.name==='报销审核员').status='idle'; toast('高风险提交已确认,托管任务完成'); }
|
||
if(a==='toggle-audit') prototypeState.auditOpen=!prototypeState.auditOpen;
|
||
if(a==='retry-task'){ prototypeState.tasks[+el.dataset.index].status='queued'; toast('任务已重新排队'); }
|
||
if(a==='config-step') prototypeState.configStep=+el.dataset.index;
|
||
if(a==='open-settings') toast('已打开系统设置指引(原型模拟)');
|
||
if(a==='recheck-permission'){ toast('权限重新检测通过'); prototypeState.modal=null; }
|
||
if(a==='annotation-fail'){ document.querySelectorAll('[data-annotation-error],[data-action="annotation-retry"]').forEach(x=>x.classList.remove('hidden')); return; }
|
||
if(a==='annotation-retry') toast('草稿已保留,请再次保存');
|
||
if(a==='model-test'){ prototypeState.loadingModel=true; renderAll(); setTimeout(()=>{ prototypeState.loadingModel=false; toast('模型连通性测试通过'); }, 420); return; }
|
||
renderAll();
|
||
}
|
||
function startHosting(){ prototypeState.modal=null; prototypeState.overlay=null; prototypeState.rpaHosting=true; prototypeState.hostingStep=0; prototypeState.hostingLogs=['托管已启动:报销审核员控制企业报销系统。']; prototypeState.tasks[0].status='running'; prototypeState.agents.find(x=>x.name==='报销审核员').status='running'; renderAll(); }
|
||
function nextHosting(){ const actions=['读取报销规则','填写金额输入框','定位提交按钮','提交报销单']; const msg=actions[Math.min(prototypeState.hostingStep,3)]; prototypeState.hostingLogs.push(new Date().toLocaleTimeString()+' '+msg); if(msg==='提交报销单'){ prototypeState.rpaHosting=false; prototypeState.highRisk=true; } else prototypeState.hostingStep++; renderAll(); }
|
||
function stopHosting(){ prototypeState.rpaHosting=false; prototypeState.highRisk=false; prototypeState.tasks[0].status='canceled'; prototypeState.agents.find(x=>x.name==='报销审核员').status='idle'; prototypeState.audits.unshift({time:'刚刚',employee:'报销审核员',app:'企业报销系统',kb:'财务报销制度',status:'canceled',text:'托管已人工终止,原因已写入审计日志'}); toast('托管已人工终止,原因已写入审计日志'); renderAll(); }
|
||
document.addEventListener('keydown', event => { if(event.key==='Escape' && (prototypeState.rpaHosting||prototypeState.highRisk)) stopHosting(); });
|
||
document.addEventListener('pointerdown', event => { const box=event.target.closest('[data-drag-box]'); if(!box) return; prototypeState.dragging={startX:event.clientX,startY:event.clientY,origX:prototypeState.draftBounds.x,origY:prototypeState.draftBounds.y}; box.setPointerCapture(event.pointerId); });
|
||
document.addEventListener('pointermove', event => { if(!prototypeState.dragging) return; prototypeState.draftBounds.x=Math.max(24, prototypeState.dragging.origX + Math.round(event.clientX-prototypeState.dragging.startX)); prototypeState.draftBounds.y=Math.max(24, prototypeState.dragging.origY + Math.round(event.clientY-prototypeState.dragging.startY)); renderAll(); });
|
||
document.addEventListener('pointerup', () => { prototypeState.dragging=false; });
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|