feat: add account roles audit and task authorization

This commit is contained in:
inman committed 2026-09-01 19:14:06 +08:00
1 parent 337aaf7c88
commit 191c1a1aad
15 files changed
+4961 -457

No files matched your search

+1165 -145
View File
File diff suppressed because it is too large. Load diff
+182 -8
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#edf2f4">
<title>AI操作台 · LianSyn-platform</title>
<link rel="stylesheet" href="styles.css?v=20260827-roster-attachment-1">
<link rel="stylesheet" href="styles.css?v=20260901-business-authorization-1">
</head>
<body>
<main class="app-shell">
@@ -16,12 +16,16 @@
<nav class="top-nav" aria-label="主菜单">
<a id="workbenchNav" class="top-nav-item is-active" aria-current="page" href="/">AI操作台</a>
<a id="historyNav" class="top-nav-item" href="/history">历史任务</a>
<a id="channelsNav" class="top-nav-item" href="/channels">AgentBus 渠道</a>
<a id="parserRoutingNav" class="top-nav-item" href="/parser-routing">解析策略</a>
<a id="operationsDashboardNav" class="top-nav-item" href="/operations-dashboard" hidden>操作数据看板</a>
<a id="channelsNav" class="top-nav-item" href="/channels" hidden>AgentBus 渠道</a>
<a id="parserRoutingNav" class="top-nav-item" href="/parser-routing" hidden>解析策略</a>
<a id="accountsNav" class="top-nav-item" href="/accounts" hidden>账号管理</a>
<a id="auditNav" class="top-nav-item" href="/audit" hidden>审计记录</a>
</nav>
<div class="top-actions">
<span id="authState" class="auth-state" hidden></span>
<button id="automationToggleButton" type="button" class="automation-toggle" hidden aria-pressed="false" title="开启后,所有来源的新任务解析通过后将跳过人工确认并自动进入 ERP"></button>
<button id="changePasswordButton" type="button" class="logout-button" hidden>修改密码</button>
<button id="logoutButton" type="button" class="logout-button" hidden>退出登录</button>
<button id="aiState" type="button" class="status-icon state-bad" title="点击查看 AI 状态详情" aria-expanded="false" aria-controls="statusDetailsPopover">
<span class="status-icon-glyph" aria-hidden="true">✦</span>
@@ -52,8 +56,8 @@
<section id="loginPanel" class="login-panel" hidden>
<div class="login-card">
<p class="eyebrow">PRIVATE CONTROL PLANE</p>
<h1>管理员登录</h1>
<p class="muted">请使用平台管理员账号进入业务操作台。</p>
<h1>平台登录</h1>
<p class="muted">请使用管理员分配的平台账号进入业务操作台。</p>
<form id="loginForm" novalidate>
<label>账号<input id="loginUsername" name="username" autocomplete="username" required></label>
<label>密码<input id="loginPassword" name="password" type="password" autocomplete="current-password" required></label>
@@ -63,6 +67,169 @@
</div>
</section>
<section id="passwordChangePanel" class="login-panel" hidden>
<div class="login-card">
<p class="eyebrow">ACCOUNT SECURITY</p>
<h1 id="passwordChangeTitle">修改密码</h1>
<p id="passwordChangeHint" class="muted">新密码至少 12 个字符。修改后,其他已登录会话将失效。</p>
<form id="passwordChangeForm" novalidate>
<label>当前密码<input id="currentPassword" type="password" autocomplete="current-password" required></label>
<label>新密码<input id="newPassword" type="password" autocomplete="new-password" minlength="12" required></label>
<label>确认新密码<input id="confirmNewPassword" type="password" autocomplete="new-password" minlength="12" required></label>
<div class="actions">
<button type="submit">保存新密码</button>
<button id="passwordChangeCancel" type="button" class="secondary-button">取消</button>
</div>
<p id="passwordChangeError" class="login-error" role="alert"></p>
</form>
</div>
</section>
<section id="accountsPage" class="channels-page" hidden>
<section class="panel channel-panel account-panel">
<div class="panel-head">
<div>
<p class="eyebrow">ACCOUNT DIRECTORY</p>
<h2>平台账号管理</h2>
<p class="muted">管理员可维护账号、密码与可执行任务类型;未授权指令会在解析和 ERP 执行前被阻止。</p>
</div>
</div>
<form id="accountForm" class="channel-form" novalidate>
<label>账号<input id="accountUsername" maxlength="160" autocomplete="off" required></label>
<label>初始密码<input id="accountPassword" type="password" minlength="12" maxlength="512" autocomplete="new-password" required></label>
<label>角色<select id="accountRole"><option value="user">普通用户</option><option value="team_lead">组长</option><option value="admin">管理员</option></select></label>
<label class="history-select-all"><input id="accountMustChangePassword" type="checkbox" checked>首次登录必须修改密码</label>
<button type="submit">创建账号</button>
</form>
<section id="accountAuthorizationPanel" class="account-authorization-panel" hidden aria-labelledby="accountAuthorizationTitle">
<div class="account-authorization-head">
<div>
<p class="eyebrow">TASK AUTHORIZATION</p>
<h3 id="accountAuthorizationTitle">任务类型授权</h3>
<p id="accountAuthorizationHint" class="muted"></p>
</div>
<div class="account-authorization-head-actions">
<button id="accountAuthorizationSelectAll" type="button" class="secondary-button">全选</button>
<button id="accountAuthorizationClear" type="button" class="secondary-button">清空</button>
</div>
</div>
<div id="accountAuthorizationTypes" class="account-authorization-types"></div>
<div class="account-authorization-footer">
<span id="accountAuthorizationCount" class="muted"></span>
<div class="actions">
<button id="accountAuthorizationSave" type="button">保存授权</button>
<button id="accountAuthorizationCancel" type="button" class="secondary-button">取消</button>
</div>
</div>
</section>
<div id="accountList" class="channel-list"></div>
<p id="accountMessage" class="channel-message" role="status"></p>
</section>
</section>
<section id="operationsDashboardPage" class="operations-dashboard-page" hidden>
<section class="panel operations-dashboard-panel">
<div class="panel-head operations-dashboard-head">
<div>
<p class="eyebrow">OPERATIONS OVERVIEW</p>
<h2>操作数据看板</h2>
<p class="muted">组长与管理员可按人员、日期、业务和结果逐层查看:谁提交了什么指令,最终完成了什么。</p>
</div>
</div>
<form id="operationsDashboardFilters" class="operations-dashboard-filters">
<label>开始日期<input id="operationsDashboardFrom" type="date" required></label>
<label>结束日期<input id="operationsDashboardTo" type="date" required></label>
<label>账号<select id="operationsDashboardActor"><option value="">全部账号</option></select></label>
<label>业务类型<select id="operationsDashboardBusiness"><option value="">全部业务</option></select></label>
<label>状态<select id="operationsDashboardStatus">
<option value="all">全部状态</option>
<option value="active">进行中</option>
<option value="completed">已完成</option>
<option value="attention">待处理</option>
<option value="failed">异常</option>
<option value="cancelled">已取消</option>
<option value="archived">已归档</option>
</select></label>
<label class="operations-dashboard-search">业务检索<input id="operationsDashboardSearch" maxlength="200" placeholder="姓名、指令内容、完成结果、团号或订单号"></label>
<div class="history-filter-actions">
<button type="submit">查询</button>
<button id="operationsDashboardReset" type="button" class="secondary-button">最近 30 天</button>
</div>
</form>
<div id="operationsDashboardActiveFilters" class="operations-dashboard-active-filters muted" aria-live="polite"></div>
<div id="operationsDashboardSummary" class="operations-dashboard-summary" aria-live="polite"></div>
<div class="operations-dashboard-breakdowns">
<section aria-labelledby="operationsDashboardDaysTitle">
<div class="operations-dashboard-section-head">
<h3 id="operationsDashboardDaysTitle">按日期穿透</h3>
<span class="muted">选择一天继续查看</span>
</div>
<div id="operationsDashboardDays" class="operations-dashboard-breakdown-list"></div>
</section>
<section aria-labelledby="operationsDashboardBusinessesTitle">
<div class="operations-dashboard-section-head">
<h3 id="operationsDashboardBusinessesTitle">按业务穿透</h3>
<span class="muted">选择业务继续查看</span>
</div>
<div id="operationsDashboardBusinesses" class="operations-dashboard-breakdown-list"></div>
</section>
</div>
<div class="operations-dashboard-grid">
<section class="operations-dashboard-column" aria-labelledby="operationsDashboardUsersTitle">
<div class="operations-dashboard-section-head">
<h3 id="operationsDashboardUsersTitle">用户工作概览</h3>
<span id="operationsDashboardUserCount" class="muted"></span>
</div>
<div id="operationsDashboardUsers" class="operations-dashboard-users"></div>
</section>
<section class="operations-dashboard-column operations-dashboard-task-column" aria-labelledby="operationsDashboardTasksTitle">
<div class="operations-dashboard-section-head">
<h3 id="operationsDashboardTasksTitle">指令操作历史</h3>
<span id="operationsDashboardTaskCount" class="muted"></span>
</div>
<div id="operationsDashboardTasks" class="operations-dashboard-tasks"></div>
<div class="history-pagination operations-dashboard-pagination">
<span id="operationsDashboardPageInfo" class="muted"></span>
<div class="history-pagination-actions">
<button id="operationsDashboardPrevious" type="button" class="secondary-button">上一页</button>
<button id="operationsDashboardNext" type="button" class="secondary-button">下一页</button>
</div>
</div>
</section>
</div>
<p id="operationsDashboardMessage" class="channel-message" role="status"></p>
</section>
<aside id="operationsDashboardDetail" class="panel operations-dashboard-detail" hidden aria-labelledby="operationsDashboardDetailTitle">
<div class="panel-head">
<div>
<p class="eyebrow">BUSINESS TRACE</p>
<h2 id="operationsDashboardDetailTitle">指令业务详情</h2>
</div>
<button id="operationsDashboardDetailClose" type="button" class="secondary-button">关闭详情</button>
</div>
<div id="operationsDashboardDetailBody" class="operations-dashboard-detail-body"></div>
</aside>
</section>
<section id="auditPage" class="channels-page" hidden>
<section class="panel channel-panel">
<div class="panel-head">
<div>
<p class="eyebrow">AUDIT TRAIL</p>
<h2>平台审计记录</h2>
<p class="muted">记录账号变更及任务操作,不记录密码或明文业务输入。</p>
</div>
</div>
<form id="auditFilters" class="history-filters">
<label>事件类型<input id="auditEventType" maxlength="200" placeholder="例如 account.password_reset"></label>
<label>实体类型<input id="auditEntityType" maxlength="120" placeholder="例如 user 或 task"></label>
<div class="history-filter-actions"><button type="submit">筛选</button><button id="auditResetButton" type="button" class="secondary-button">重置</button></div>
</form>
<div id="auditList" class="channel-list"></div>
<p id="auditMessage" class="channel-message" role="status"></p>
</section>
</section>
<section id="channelsPage" class="channels-page" hidden>
<section id="channelPanel" class="panel channel-panel">
<div class="panel-head">
@@ -166,6 +333,13 @@
<option value="reconciliation_pending">待回查</option>
</select>
</label>
<label>
范围
<select id="historyArchiveInput">
<option value="active">未归档</option>
<option value="archived">已归档</option>
</select>
</label>
<div class="history-filter-actions">
<button type="submit">筛选</button>
<button id="historyResetButton" type="button" class="secondary-button">重置</button>
@@ -178,7 +352,7 @@
</label>
<span id="historySelectionSummary" class="muted">已选 0 个</span>
<span id="historyBatchMessage" class="history-batch-message" role="status" aria-live="polite"></span>
<button id="historyDeleteSelectedButton" type="button" class="danger-button" disabled>删除所选</button>
<button id="historyDeleteSelectedButton" type="button" class="danger-button" disabled>归档所选</button>
</div>
<div id="taskCards" class="task-cards"></div>
<div id="historyPagination" class="history-pagination" hidden>
@@ -196,7 +370,7 @@
<div class="detail-panel-actions">
<span id="taskState" class="muted">未创建</span>
<button id="confirmTaskButton" type="button" hidden disabled>确认并提交到 ERP 插件</button>
<button id="deleteTaskButton" type="button" class="danger-button" disabled>删除任务</button>
<button id="deleteTaskButton" type="button" class="danger-button" disabled>归档任务</button>
</div>
</div>
<div id="taskStages" class="task-stages" aria-label="项目处理阶段"></div>
@@ -205,6 +379,6 @@
</section>
</main>
<script src="app.js?v=20260830-team-order-validation-scope-1"></script>
<script src="app.js?v=20260901-business-authorization-1"></script>
</body>
</html>
+488 -1
View File
@@ -551,7 +551,8 @@ textarea {
font-weight: 700;
}
.channel-form input {
.channel-form input,
.channel-form select {
min-height: 34px;
padding: 6px 8px;
font-size: 12px;
@@ -627,6 +628,122 @@ textarea {
font-size: 11px;
}
.channel-row-actions select {
min-height: 28px;
padding: 0 8px;
font-size: 11px;
}
.account-panel {
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
}
.account-authorization-summary {
color: var(--ink) !important;
}
.account-authorization-panel {
display: grid;
gap: 12px;
padding: 12px;
border: 1px solid #aebcc4;
border-radius: 8px;
background: #ffffff;
}
.account-authorization-head,
.account-authorization-footer {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.account-authorization-head h3 {
margin-top: 2px;
font-size: 15px;
}
.account-authorization-head .muted {
max-width: 760px;
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
}
.account-authorization-head-actions,
.account-authorization-footer .actions {
display: flex;
flex: 0 0 auto;
gap: 7px;
}
.account-authorization-head-actions button,
.account-authorization-footer button {
min-height: 30px;
padding: 0 10px;
font-size: 11px;
}
.account-authorization-types {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 7px;
max-height: 260px;
overflow: auto;
}
.account-authorization-type {
min-width: 0;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 7px;
background: var(--surface-muted);
cursor: pointer;
}
.account-authorization-type:hover,
.account-authorization-type.is-selected {
border-color: #8197a2;
background: #edf3f5;
}
.account-authorization-type input {
width: 15px;
height: 15px;
margin-top: 1px;
accent-color: var(--ink);
}
.account-authorization-type-copy {
min-width: 0;
display: grid;
gap: 3px;
}
.account-authorization-type-copy strong,
.account-authorization-type-copy small {
min-width: 0;
overflow-wrap: anywhere;
}
.account-authorization-type-copy strong {
font-size: 12px;
line-height: 1.35;
}
.account-authorization-type-copy small {
font-size: 10px;
}
.account-authorization-footer {
align-items: center;
padding-top: 2px;
}
.panel {
min-width: 0;
min-height: 0;
@@ -1882,6 +1999,16 @@ textarea {
flex-direction: column;
}
.account-authorization-head,
.account-authorization-footer {
align-items: stretch;
flex-direction: column;
}
.account-authorization-types {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric {
border-right: 0;
border-bottom: 1px solid var(--line);
@@ -2926,6 +3053,366 @@ textarea {
max-height: none;
}
.audit-page-active .channel-row {
display: grid;
align-items: stretch;
}
.audit-page-active .channel-row > pre {
max-height: 180px;
margin: 0;
}
.operations-dashboard-page {
min-width: 0;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 14px;
overflow: auto;
}
.operations-dashboard-panel {
min-height: 100%;
display: grid;
grid-template-rows: auto auto auto auto auto minmax(0, 1fr) auto;
gap: 14px;
}
.operations-dashboard-head {
align-items: flex-start;
}
.operations-dashboard-filters {
display: grid;
grid-template-columns: repeat(5, minmax(115px, 0.7fr)) minmax(220px, 1.5fr) auto;
gap: 10px;
align-items: end;
}
.operations-dashboard-filters label {
display: grid;
gap: 5px;
color: var(--muted);
font-size: 11px;
}
.operations-dashboard-filters input,
.operations-dashboard-filters select {
width: 100%;
}
.operations-dashboard-active-filters {
min-height: 17px;
font-size: 11px;
}
.operations-dashboard-summary {
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 9px;
}
.operations-dashboard-stat {
min-width: 0;
display: grid;
gap: 3px;
padding: 12px 13px;
border: 1px solid var(--line);
border-radius: 10px;
background: #f8fafb;
color: var(--ink);
font: inherit;
text-align: left;
}
.operations-dashboard-stat[data-operations-status] {
cursor: pointer;
}
.operations-dashboard-stat[data-operations-status]:hover,
.operations-dashboard-stat[data-operations-status].is-active {
border-color: #78a99b;
background: #eef7f3;
}
.operations-dashboard-stat[data-operations-status].is-active {
box-shadow: inset 0 0 0 1px #78a99b;
}
.operations-dashboard-stat > span,
.operations-dashboard-stat > small {
color: var(--muted);
font-size: 10px;
}
.operations-dashboard-stat > strong {
color: var(--ink);
font-size: 23px;
line-height: 1.05;
}
.operations-dashboard-breakdowns {
min-width: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.operations-dashboard-breakdowns > section {
min-width: 0;
display: grid;
gap: 8px;
padding: 10px 11px;
border: 1px solid var(--line);
border-radius: 11px;
background: #fbfcfc;
}
.operations-dashboard-breakdown-list {
min-width: 0;
display: flex;
gap: 7px;
overflow-x: auto;
padding-bottom: 2px;
}
.operations-dashboard-breakdown-row {
min-width: 155px;
display: grid;
gap: 4px;
padding: 8px 9px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
color: var(--ink);
font: inherit;
text-align: left;
}
.operations-dashboard-breakdown-row:hover {
border-color: #8eb5a8;
background: #f3f9f6;
}
.operations-dashboard-breakdown-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 11px;
}
.operations-dashboard-breakdown-heading b {
color: var(--accent);
font-size: 15px;
}
.operations-dashboard-grid {
min-height: 0;
display: grid;
grid-template-columns: minmax(260px, 0.72fr) minmax(420px, 1.5fr);
gap: 12px;
}
.operations-dashboard-column {
min-width: 0;
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 9px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: #fbfcfc;
}
.operations-dashboard-task-column {
grid-template-rows: auto minmax(0, 1fr) auto;
}
.operations-dashboard-section-head,
.operations-dashboard-row-heading {
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.operations-dashboard-section-head h3 {
margin: 0;
font-size: 13px;
}
.operations-dashboard-users,
.operations-dashboard-tasks {
min-height: 0;
display: grid;
align-content: start;
gap: 8px;
overflow: auto;
}
.operations-dashboard-user-row,
.operations-dashboard-task-row {
width: 100%;
min-width: 0;
display: grid;
gap: 6px;
padding: 10px;
border: 1px solid var(--line);
border-radius: 9px;
color: var(--ink);
background: #fff;
text-align: left;
}
.operations-dashboard-user-row:hover,
.operations-dashboard-task-row:hover,
.operations-dashboard-task-row.is-selected {
border-color: #8eb5a8;
background: #f3f9f6;
}
.operations-dashboard-user-metrics {
display: flex;
flex-wrap: wrap;
gap: 5px 12px;
color: var(--muted);
font-size: 11px;
}
.operations-dashboard-user-metrics .is-error {
color: var(--bad);
}
.operations-dashboard-task-row p {
margin: 0;
}
.operations-dashboard-instruction {
display: -webkit-box;
overflow: hidden;
color: var(--ink);
font-size: 12px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.operations-dashboard-result {
display: -webkit-box;
overflow: hidden;
color: #356a5a;
font-size: 12px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.operations-dashboard-detail {
display: grid;
gap: 14px;
}
.operations-dashboard-detail-body {
display: grid;
gap: 12px;
}
.operations-dashboard-detail-section {
min-width: 0;
display: grid;
gap: 8px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.operations-dashboard-detail-section:first-child {
padding-top: 0;
border-top: 0;
}
.operations-dashboard-detail-section h3 {
margin: 0;
font-size: 13px;
}
.operations-dashboard-detail-facts {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
}
.operations-dashboard-original-input,
.operations-dashboard-input-item pre,
.operations-dashboard-result-detail {
max-height: 260px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.operations-dashboard-result-detail {
padding: 11px 12px;
border: 1px solid #bdd7cd;
border-radius: 8px;
background: #f3f9f6;
color: #285c4d;
font-family: inherit;
line-height: 1.55;
}
.operations-dashboard-input-history,
.operations-dashboard-attachments {
display: grid;
gap: 8px;
}
.operations-dashboard-input-item,
.operations-dashboard-attachment-row {
min-width: 0;
display: grid;
gap: 5px;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fafbfb;
}
@media (max-width: 1180px) {
.operations-dashboard-filters {
grid-template-columns: repeat(3, minmax(140px, 1fr));
}
.operations-dashboard-summary {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.operations-dashboard-detail-facts {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 820px) {
.operations-dashboard-grid,
.operations-dashboard-filters,
.operations-dashboard-breakdowns {
grid-template-columns: minmax(0, 1fr);
}
.operations-dashboard-summary,
.operations-dashboard-detail-facts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.operations-dashboard-column {
max-height: none;
}
}
.history-page .workbench-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
}