1323 lines
59 KiB
Vue
1323 lines
59 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||
import { message } from "ant-design-vue";
|
||
import {
|
||
AppstoreOutlined,
|
||
AuditOutlined,
|
||
ClusterOutlined,
|
||
DashboardOutlined,
|
||
SafetyOutlined,
|
||
SettingOutlined,
|
||
TeamOutlined,
|
||
ToolOutlined,
|
||
UserOutlined,
|
||
} from "@ant-design/icons-vue";
|
||
import {
|
||
createAdminBackup,
|
||
fetchAdminBackups,
|
||
fetchBossAdminBackoffice,
|
||
postAdminAccess,
|
||
postRiskAction,
|
||
postSkillLifecycleRequest,
|
||
restoreAdminBackup,
|
||
type BossAdminBackofficePayload,
|
||
type BossAdminBackupSnapshot,
|
||
type BossAdminBackupStatus,
|
||
} from "./api/bossAdmin";
|
||
|
||
type AdminRecord = Record<string, unknown>;
|
||
type AdminSurface = "platform" | "enterprise";
|
||
|
||
const loading = ref(true);
|
||
const mutating = ref(false);
|
||
const error = ref("");
|
||
const adminSurface = ref<AdminSurface>("platform");
|
||
const activeKey = ref("platform-overview");
|
||
const payload = ref<BossAdminBackofficePayload | null>(null);
|
||
const backupLoading = ref(false);
|
||
const backupSnapshots = ref<BossAdminBackupSnapshot[]>([]);
|
||
const backupStatus = ref<BossAdminBackupStatus | null>(null);
|
||
const backupReason = ref("manual");
|
||
|
||
const companyForm = reactive({
|
||
companyId: "",
|
||
name: "",
|
||
ownerAccount: "",
|
||
successOwnerAccount: "",
|
||
planTier: "enterprise",
|
||
contractExpiresAt: "",
|
||
note: "",
|
||
});
|
||
|
||
const accountForm = reactive({
|
||
account: "",
|
||
displayName: "",
|
||
role: "member",
|
||
password: "",
|
||
companyId: "",
|
||
});
|
||
|
||
const grantForm = reactive({
|
||
account: "",
|
||
scope: "device",
|
||
targetId: "",
|
||
templateId: "developer",
|
||
permissions: ["device.view"],
|
||
expiresAt: "",
|
||
note: "",
|
||
});
|
||
|
||
const riskForm = reactive({
|
||
ownerAccount: "",
|
||
slaDueAt: "",
|
||
note: "",
|
||
});
|
||
|
||
const skillRequestForm = reactive({
|
||
action: "update",
|
||
deviceId: "",
|
||
skillId: "",
|
||
sourceUrl: "",
|
||
trustedSourceId: "",
|
||
targetVersion: "",
|
||
rollbackToVersion: "",
|
||
lockedVersion: "",
|
||
checksum: "",
|
||
note: "",
|
||
});
|
||
|
||
const defaultBackofficeInsights: BossAdminBackofficePayload["insights"] = {
|
||
onboardingSteps: [],
|
||
serviceStatuses: [],
|
||
openingPreview: [],
|
||
deliveryChecklist: [],
|
||
recentCompanies: [],
|
||
customerHealthRows: [],
|
||
riskAggregates: [],
|
||
customerFollowups: [],
|
||
enterpriseGoals: [],
|
||
organizationUnits: [],
|
||
departmentProgress: [],
|
||
masterAgentSummary: [],
|
||
permissionHighlights: [],
|
||
agentFlowSteps: [],
|
||
skillUsageAudit: [],
|
||
recoveryActions: [],
|
||
backupStatus: {},
|
||
capabilitySummary: {},
|
||
surface: "platform",
|
||
};
|
||
|
||
const menuIconMap: Record<string, unknown> = {
|
||
"platform-overview": DashboardOutlined,
|
||
"platform-provisioning": ClusterOutlined,
|
||
"platform-customer-plans": SafetyOutlined,
|
||
"platform-devices": AppstoreOutlined,
|
||
"platform-customer-success": TeamOutlined,
|
||
"platform-risk": AuditOutlined,
|
||
"platform-audit": AuditOutlined,
|
||
"platform-billing": SafetyOutlined,
|
||
"platform-settings": SettingOutlined,
|
||
"enterprise-overview": DashboardOutlined,
|
||
"enterprise-members": UserOutlined,
|
||
"enterprise-devices-agents": AppstoreOutlined,
|
||
"enterprise-agent-flows": ClusterOutlined,
|
||
"enterprise-skill": ToolOutlined,
|
||
"enterprise-risk-backup": AuditOutlined,
|
||
"enterprise-backup": SafetyOutlined,
|
||
"enterprise-settings": SettingOutlined,
|
||
};
|
||
|
||
const menuTree = computed(() => payload.value?.menuTree ?? []);
|
||
const summary = computed(() => payload.value?.workbench.summary ?? {});
|
||
const tenants = computed(() => payload.value?.tenants ?? []);
|
||
const users = computed(() => payload.value?.users ?? []);
|
||
const roles = computed(() => payload.value?.roles.builtInRoles ?? []);
|
||
const templates = computed(() => payload.value?.roles.permissionTemplates ?? []);
|
||
const devices = computed(() => payload.value?.resourceGroups.devices ?? []);
|
||
const projects = computed(() => payload.value?.resourceGroups.projects ?? []);
|
||
const skills = computed(() => payload.value?.resourceGroups.skills ?? []);
|
||
const insights = computed(() => payload.value?.insights ?? defaultBackofficeInsights);
|
||
const risks = computed(() => payload.value?.audit.risks ?? []);
|
||
const notifications = computed(() => payload.value?.workbench.notifications ?? []);
|
||
const riskTimeline = computed(() => payload.value?.audit.riskTimeline ?? []);
|
||
const auditLogs = computed(() => payload.value?.audit.permissionLogs ?? []);
|
||
const grants = computed(() => payload.value?.resourceGroups.grants ?? { devices: [], projects: [], skills: [] });
|
||
|
||
const currentSectionTitle = computed(() => menuTree.value.find((item) => item.key === activeKey.value)?.label ?? "总览");
|
||
const currentCompanyName = computed(() => text(payload.value?.currentCompany?.name, "当前企业"));
|
||
const surfaceLabel = computed(() => (adminSurface.value === "platform" ? "Boss 平台总后台" : "企业管理后台"));
|
||
const surfaceHint = computed(() =>
|
||
adminSurface.value === "platform"
|
||
? "给企业开户、看全局风险、做客户成功和平台治理。"
|
||
: "企业内部管理账号、设备、项目、Agent、Skill、风险与备份。",
|
||
);
|
||
|
||
const platformMetricCards = computed(() => [
|
||
{ label: "企业数", value: numberValue(summary.value.companies), tone: "green" },
|
||
{ label: "账号数", value: numberValue(summary.value.accounts), tone: "black" },
|
||
{ label: "在线电脑", value: numberValue(summary.value.onlineDevices), tone: "green" },
|
||
{ label: "开放风险", value: numberValue(summary.value.openRisks), tone: "red" },
|
||
{ label: "待处理通知", value: numberValue(summary.value.openNotifications), tone: "orange" },
|
||
]);
|
||
|
||
const enterpriseMetricCards = computed(() => [
|
||
{ label: "成员账号", value: numberValue(summary.value.accounts), tone: "black" },
|
||
{ label: "已授权电脑", value: numberValue(summary.value.devices), tone: "green" },
|
||
{ label: "在线电脑", value: numberValue(summary.value.onlineDevices), tone: "green" },
|
||
{ label: "项目线程", value: projects.value.length, tone: "black" },
|
||
{ label: "风险事项", value: numberValue(summary.value.openRisks), tone: "red" },
|
||
]);
|
||
|
||
const grantRows = computed(() => [
|
||
...grants.value.devices.map((grant) => ({ ...grant, scopeLabel: "设备", targetLabel: text(grant.deviceId) })),
|
||
...grants.value.projects.map((grant) => ({ ...grant, scopeLabel: "项目", targetLabel: text(grant.projectId) })),
|
||
...grants.value.skills.map((grant) => ({ ...grant, scopeLabel: "Skill", targetLabel: text(grant.skillId) })),
|
||
]);
|
||
|
||
const selectedTemplate = computed(() =>
|
||
templates.value.find((item) => text(item.templateId) === grantForm.templateId),
|
||
);
|
||
|
||
const selectedScopePermissions = computed(() => {
|
||
if (grantForm.scope === "project") return selectedTemplate.value?.projectPermissions ?? ["project.view"];
|
||
if (grantForm.scope === "skill") return selectedTemplate.value?.skillPermissions ?? ["skill.view"];
|
||
return selectedTemplate.value?.devicePermissions ?? ["device.view"];
|
||
});
|
||
|
||
const selectedScopePermissionPlaceholder = computed(() =>
|
||
Array.isArray(selectedScopePermissions.value) ? selectedScopePermissions.value.join(" / ") : "device.view",
|
||
);
|
||
|
||
const grantTargetOptions = computed(() => {
|
||
if (grantForm.scope === "project") {
|
||
return projects.value.map((project) => ({ id: text(project.id), label: text(project.name) }));
|
||
}
|
||
if (grantForm.scope === "skill") {
|
||
return skills.value.map((skill) => ({ id: text(skill.skillId), label: text(skill.name) }));
|
||
}
|
||
return devices.value.map((device) => ({ id: text(device.id), label: text(device.name) }));
|
||
});
|
||
|
||
const provisioningKpis = computed(() => [
|
||
{ label: "开通状态", value: companyForm.companyId || "草稿", tone: "black" },
|
||
{ label: "授权设备", value: `${Math.max(1, devices.value.length || 1)} 台`, tone: "green" },
|
||
{ label: "授权账号", value: `${Math.max(1, users.value.length || 1)} 个`, tone: "green" },
|
||
{ label: "到期时间", value: companyForm.contractExpiresAt || "待设置", tone: "orange" },
|
||
]);
|
||
|
||
const effectiveBackupStatus = computed(() => backupStatus.value ?? insights.value.backupStatus);
|
||
|
||
async function loadBackoffice(surface: AdminSurface = adminSurface.value) {
|
||
loading.value = true;
|
||
error.value = "";
|
||
try {
|
||
const nextPayload = await fetchBossAdminBackoffice(surface);
|
||
payload.value = nextPayload;
|
||
adminSurface.value = nextPayload.surface;
|
||
ensureActiveMenu(nextPayload);
|
||
} catch (err) {
|
||
const reason = err instanceof Error ? err.message : "后台数据加载失败";
|
||
if (surface === "platform" && reason === "FORBIDDEN") {
|
||
adminSurface.value = "enterprise";
|
||
const enterprisePayload = await fetchBossAdminBackoffice("enterprise");
|
||
payload.value = enterprisePayload;
|
||
ensureActiveMenu(enterprisePayload);
|
||
} else {
|
||
error.value = reason;
|
||
}
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function ensureActiveMenu(nextPayload: BossAdminBackofficePayload) {
|
||
const keys = nextPayload.menuTree.map((item) => item.key);
|
||
if (!keys.includes(activeKey.value)) {
|
||
activeKey.value = keys[0] ?? (nextPayload.surface === "platform" ? "platform-overview" : "enterprise-overview");
|
||
}
|
||
}
|
||
|
||
function setSurface(surface: AdminSurface) {
|
||
if (adminSurface.value === surface && payload.value) return;
|
||
adminSurface.value = surface;
|
||
activeKey.value = surface === "platform" ? "platform-overview" : "enterprise-overview";
|
||
void loadBackoffice(surface);
|
||
}
|
||
|
||
function selectMenu(key: string) {
|
||
activeKey.value = key;
|
||
if (key === "enterprise-backup" || key === "enterprise-risk-backup") {
|
||
void loadBackupSnapshots();
|
||
}
|
||
}
|
||
|
||
function menuIcon(key: string) {
|
||
return menuIconMap[key] ?? TeamOutlined;
|
||
}
|
||
|
||
function text(value: unknown, fallback = "-") {
|
||
if (value === null || value === undefined || value === "") return fallback;
|
||
return String(value);
|
||
}
|
||
|
||
function numberValue(value: unknown) {
|
||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||
}
|
||
|
||
function permissionText(value: unknown) {
|
||
return Array.isArray(value) ? value.join(" / ") : text(value);
|
||
}
|
||
|
||
function statusColor(value: unknown) {
|
||
const current = text(value, "").toLowerCase();
|
||
if (current === "online" || current === "active" || current === "resolved") return "green";
|
||
if (current === "disabled" || current === "offline") return "default";
|
||
return "orange";
|
||
}
|
||
|
||
function riskColor(value: unknown) {
|
||
const severity = text(value, "").toLowerCase();
|
||
if (severity === "critical") return "red";
|
||
if (severity === "warning") return "orange";
|
||
return "blue";
|
||
}
|
||
|
||
function formatBytes(value: unknown) {
|
||
const bytes = typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||
if (bytes < 1024) return `${bytes} B`;
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||
}
|
||
|
||
function resetCompanyForm() {
|
||
Object.assign(companyForm, {
|
||
companyId: "",
|
||
name: "",
|
||
ownerAccount: "",
|
||
successOwnerAccount: "",
|
||
planTier: "enterprise",
|
||
contractExpiresAt: "",
|
||
note: "",
|
||
});
|
||
}
|
||
|
||
function resetAccountForm() {
|
||
Object.assign(accountForm, {
|
||
account: "",
|
||
displayName: "",
|
||
role: "member",
|
||
password: "",
|
||
companyId: "",
|
||
});
|
||
}
|
||
|
||
async function runMutation(label: string, task: () => Promise<unknown>) {
|
||
mutating.value = true;
|
||
const hide = message.loading(`${label}中...`, 0);
|
||
try {
|
||
await task();
|
||
hide();
|
||
message.success(`${label}完成`);
|
||
await loadBackoffice(adminSurface.value);
|
||
} catch (err) {
|
||
hide();
|
||
message.error(`${label}失败:${err instanceof Error ? err.message : "UNKNOWN_ERROR"}`);
|
||
} finally {
|
||
mutating.value = false;
|
||
}
|
||
}
|
||
|
||
async function saveCompany() {
|
||
await runMutation("新建租户", async () => {
|
||
await postAdminAccess({
|
||
action: "upsert_company",
|
||
...companyForm,
|
||
});
|
||
resetCompanyForm();
|
||
});
|
||
}
|
||
|
||
async function setCompanyStatus(record: AdminRecord, status: "active" | "disabled") {
|
||
await runMutation(status === "active" ? "启用租户" : "停用租户", () =>
|
||
postAdminAccess({
|
||
action: "set_company_status",
|
||
companyId: text(record.companyId),
|
||
status,
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function saveAccount() {
|
||
await runMutation("新建账号", async () => {
|
||
await postAdminAccess({
|
||
action: "upsert_account",
|
||
...accountForm,
|
||
});
|
||
resetAccountForm();
|
||
});
|
||
}
|
||
|
||
async function setAccountStatus(record: AdminRecord, status: "active" | "disabled") {
|
||
await runMutation(status === "active" ? "启用账号" : "停用账号", () =>
|
||
postAdminAccess({
|
||
action: "set_account_status",
|
||
account: text(record.account),
|
||
status,
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function resetPassword(record: AdminRecord) {
|
||
const password = window.prompt(`请输入 ${text(record.account)} 的新密码`);
|
||
if (!password) return;
|
||
await runMutation("重置密码", () =>
|
||
postAdminAccess({
|
||
action: "reset_account_password",
|
||
account: text(record.account),
|
||
password,
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function reclaimAccount(record: AdminRecord) {
|
||
if (!window.confirm(`确认离职回收 ${text(record.account)}?这会停用账号并清理授权。`)) return;
|
||
await runMutation("离职回收", () =>
|
||
postAdminAccess({
|
||
action: "reclaim_account",
|
||
account: text(record.account),
|
||
reason: "enterprise-admin-web",
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function submitGrant() {
|
||
const permissions = grantForm.permissions.length > 0 ? grantForm.permissions : selectedScopePermissions.value;
|
||
const base = {
|
||
account: grantForm.account,
|
||
permissions,
|
||
expiresAt: grantForm.expiresAt,
|
||
note: grantForm.note,
|
||
};
|
||
await runMutation("分配资源", async () => {
|
||
if (grantForm.scope === "project") {
|
||
await postAdminAccess({ action: "grant_project", ...base, projectId: grantForm.targetId });
|
||
} else if (grantForm.scope === "skill") {
|
||
await postAdminAccess({ action: "grant_skill", ...base, skillId: grantForm.targetId });
|
||
} else {
|
||
await postAdminAccess({ action: "grant_device", ...base, deviceId: grantForm.targetId });
|
||
}
|
||
});
|
||
}
|
||
|
||
async function applyPermissionTemplate() {
|
||
await runMutation("套用权限模板", () =>
|
||
postAdminAccess({
|
||
action: "apply_template",
|
||
account: grantForm.account,
|
||
templateId: grantForm.templateId,
|
||
deviceIds: grantForm.scope === "device" ? [grantForm.targetId] : [],
|
||
projectIds: grantForm.scope === "project" ? [grantForm.targetId] : [],
|
||
skillIds: grantForm.scope === "skill" ? [grantForm.targetId] : [],
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function revokeGrant(record: AdminRecord) {
|
||
if (!window.confirm(`确认撤销授权 ${text(record.grantId)}?`)) return;
|
||
await runMutation("撤销授权", () =>
|
||
postAdminAccess({
|
||
action: "revoke_grant",
|
||
grantId: text(record.grantId),
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function handleRisk(record: AdminRecord, action: string) {
|
||
await runMutation(
|
||
action === "assign_owner"
|
||
? "指派负责人"
|
||
: action === "set_sla"
|
||
? "设置 SLA"
|
||
: action === "ack"
|
||
? "确认风险"
|
||
: action === "resolve"
|
||
? "关闭风险"
|
||
: "创建工单",
|
||
() =>
|
||
postRiskAction({
|
||
riskId: text(record.riskId),
|
||
action,
|
||
ownerAccount: riskForm.ownerAccount,
|
||
slaDueAt: riskForm.slaDueAt,
|
||
note: riskForm.note,
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function createSkillRequest() {
|
||
await runMutation("创建 Skill 请求", () =>
|
||
postSkillLifecycleRequest({
|
||
...skillRequestForm,
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function loadBackupSnapshots() {
|
||
backupLoading.value = true;
|
||
try {
|
||
const result = await fetchAdminBackups();
|
||
backupSnapshots.value = result.snapshots;
|
||
backupStatus.value = result.status;
|
||
} catch (err) {
|
||
message.error(`备份快照加载失败:${err instanceof Error ? err.message : "UNKNOWN_ERROR"}`);
|
||
} finally {
|
||
backupLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function createStateSnapshot() {
|
||
await runMutation("创建状态快照", async () => {
|
||
await createAdminBackup(backupReason.value || "manual");
|
||
await loadBackupSnapshots();
|
||
});
|
||
}
|
||
|
||
async function restoreStateSnapshot(record: AdminRecord) {
|
||
const snapshotId = text(record.snapshotId, "");
|
||
if (!snapshotId) return;
|
||
if (!window.confirm(`确认恢复到快照 ${snapshotId}?系统会先自动创建恢复前快照。`)) return;
|
||
await runMutation("恢复状态快照", async () => {
|
||
await restoreAdminBackup(snapshotId);
|
||
await loadBackupSnapshots();
|
||
});
|
||
}
|
||
|
||
watch(activeKey, (key) => {
|
||
if (key === "enterprise-backup" || key === "enterprise-risk-backup") {
|
||
void loadBackupSnapshots();
|
||
}
|
||
});
|
||
|
||
onMounted(async () => {
|
||
await loadBackoffice("platform");
|
||
if (activeKey.value === "enterprise-backup" || activeKey.value === "enterprise-risk-backup") {
|
||
await loadBackupSnapshots();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<a-config-provider
|
||
:theme="{
|
||
token: {
|
||
colorPrimary: '#10b981',
|
||
borderRadius: 18,
|
||
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
||
},
|
||
}"
|
||
>
|
||
<div class="boss-admin-shell" :class="`surface-${adminSurface}`">
|
||
<aside class="boss-admin-sidebar">
|
||
<div class="boss-admin-brand">
|
||
<div class="boss-admin-brand-mark">B</div>
|
||
<div>
|
||
<h1>Boss 管理后台</h1>
|
||
<p>企业账号、设备与风险统一治理</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="boss-admin-surface-switch" aria-label="后台类型切换">
|
||
<button
|
||
class="boss-admin-surface-card"
|
||
:class="{ active: adminSurface === 'platform' }"
|
||
type="button"
|
||
@click="setSurface('platform')"
|
||
>
|
||
<span>Boss 平台总后台</span>
|
||
<small>开户 / 全局风险 / 客户成功</small>
|
||
</button>
|
||
<button
|
||
class="boss-admin-surface-card"
|
||
:class="{ active: adminSurface === 'enterprise' }"
|
||
type="button"
|
||
@click="setSurface('enterprise')"
|
||
>
|
||
<span>企业管理后台</span>
|
||
<small>成员 / 电脑 / Skill / 备份</small>
|
||
</button>
|
||
</div>
|
||
|
||
<nav class="boss-admin-menu" aria-label="后台菜单">
|
||
<span class="boss-admin-visually-hidden">
|
||
平台总览 企业开通 客户与套餐 全局设备 客户成功 全局风险 服务连接状态 最近开通企业 开通预览 交付检查 客户健康列表 风险聚合 客户成功跟进记录 企业总览 组织与成员 设备与项目 Agent 与流程 Skill 中心 风险与审计 备份与回退 经营目标 部门进展 主 Agent 摘要 组织结构 权限详情 使用审计 业务级回退
|
||
</span>
|
||
<button
|
||
v-for="item in menuTree"
|
||
:key="item.key"
|
||
class="boss-admin-menu-item"
|
||
:class="{ active: activeKey === item.key }"
|
||
type="button"
|
||
@click="selectMenu(item.key)"
|
||
>
|
||
<component :is="menuIcon(item.key)" />
|
||
<span>{{ item.label }}</span>
|
||
</button>
|
||
</nav>
|
||
</aside>
|
||
|
||
<main class="boss-admin-main">
|
||
<header class="boss-admin-header">
|
||
<div>
|
||
<p class="boss-admin-eyebrow">{{ surfaceLabel }} · {{ surfaceHint }}</p>
|
||
<h2>{{ currentSectionTitle }}</h2>
|
||
</div>
|
||
<div class="boss-admin-header-actions">
|
||
<a-tag :color="adminSurface === 'platform' ? 'green' : 'blue'">
|
||
{{ adminSurface === "platform" ? "highest_admin" : currentCompanyName }}
|
||
</a-tag>
|
||
<a-button :loading="loading" @click="loadBackoffice(adminSurface)">刷新</a-button>
|
||
</div>
|
||
</header>
|
||
|
||
<a-alert
|
||
v-if="error"
|
||
class="boss-admin-alert"
|
||
type="error"
|
||
show-icon
|
||
:message="`后台数据加载失败:${error}`"
|
||
/>
|
||
|
||
<a-spin :spinning="loading">
|
||
<section v-if="activeKey === 'platform-overview'" class="boss-admin-section-grid">
|
||
<a-card class="boss-admin-hero" :bordered="false">
|
||
<p class="boss-admin-eyebrow">平台总览</p>
|
||
<h3>公司、账号、电脑节点和风险一张图看清</h3>
|
||
<div class="boss-admin-metrics">
|
||
<div v-for="metric in platformMetricCards" :key="metric.label" class="boss-admin-metric" :class="metric.tone">
|
||
<span>{{ metric.label }}</span>
|
||
<strong>{{ metric.value }}</strong>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="关键风险" :bordered="false">
|
||
<a-table size="small" :pagination="false" :data-source="risks.slice(0, 6)" row-key="riskId">
|
||
<a-table-column title="风险" data-index="title" />
|
||
<a-table-column title="级别">
|
||
<template #default="{ record }">
|
||
<a-tag :color="riskColor(record.severity)">{{ text(record.severity) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="对象" data-index="deviceId" />
|
||
<a-table-column title="时间" data-index="lastSeenAt" />
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<a-card title="客户健康排行" :bordered="false">
|
||
<a-table size="small" :pagination="false" :data-source="tenants.slice(0, 8)" row-key="companyId">
|
||
<a-table-column title="公司" data-index="name" />
|
||
<a-table-column title="账号" data-index="accountCount" />
|
||
<a-table-column title="设备" data-index="deviceCount" />
|
||
<a-table-column title="风险" data-index="openRiskCount" />
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<a-card title="节点健康" :bordered="false">
|
||
<a-table size="small" :pagination="false" :data-source="devices.slice(0, 8)" row-key="id">
|
||
<a-table-column title="电脑" data-index="name" />
|
||
<a-table-column title="企业" data-index="companyName" />
|
||
<a-table-column title="状态">
|
||
<template #default="{ record }">
|
||
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="CLI" data-index="codexCliOnline" />
|
||
<a-table-column title="GUI" data-index="codexGuiOnline" />
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<a-card title="服务连接状态" :bordered="false">
|
||
<div class="boss-admin-status-list">
|
||
<div v-for="item in insights.serviceStatuses" :key="text(item.label)" class="boss-admin-status-row">
|
||
<span>{{ text(item.label) }}</span>
|
||
<a-tag :color="text(item.tone, 'green')">{{ text(item.value) }}</a-tag>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="最近开通企业" :bordered="false">
|
||
<a-list :data-source="insights.recentCompanies">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta :title="text(item.label)" :description="text(item.note)" />
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-provisioning'" class="boss-admin-section-grid">
|
||
<a-card class="boss-admin-hero" :bordered="false">
|
||
<p class="boss-admin-eyebrow">企业开通</p>
|
||
<h3>为客户创建企业空间、老板账号、套餐授权与首批设备</h3>
|
||
<div class="boss-admin-steps">
|
||
<div v-for="(step, index) in insights.onboardingSteps" :key="step" class="boss-admin-step" :class="{ active: index === 2 }">
|
||
<span>{{ index + 1 }}</span>
|
||
<strong>{{ step }}</strong>
|
||
</div>
|
||
</div>
|
||
<div class="boss-admin-metrics compact">
|
||
<div v-for="metric in provisioningKpis" :key="metric.label" class="boss-admin-metric" :class="metric.tone">
|
||
<span>{{ metric.label }}</span>
|
||
<strong>{{ metric.value }}</strong>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="企业开通" :bordered="false">
|
||
<a-form layout="vertical" class="boss-admin-form">
|
||
<a-form-item label="企业 ID">
|
||
<a-input v-model:value="companyForm.companyId" placeholder="acme" />
|
||
</a-form-item>
|
||
<a-form-item label="公司名称">
|
||
<a-input v-model:value="companyForm.name" placeholder="客户公司显示名称" />
|
||
</a-form-item>
|
||
<a-form-item label="老板账号">
|
||
<a-input v-model:value="companyForm.ownerAccount" placeholder="owner@example.com" />
|
||
</a-form-item>
|
||
<a-form-item label="客户成功负责人">
|
||
<a-input v-model:value="companyForm.successOwnerAccount" placeholder="cs@boss.com" />
|
||
</a-form-item>
|
||
<a-form-item label="套餐">
|
||
<a-select v-model:value="companyForm.planTier">
|
||
<a-select-option value="trial">trial</a-select-option>
|
||
<a-select-option value="standard">standard</a-select-option>
|
||
<a-select-option value="enterprise">enterprise</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="合同到期">
|
||
<a-input v-model:value="companyForm.contractExpiresAt" placeholder="2027-05-01T00:00:00+08:00" />
|
||
</a-form-item>
|
||
<a-button type="primary" block :loading="mutating" @click="saveCompany">新建租户</a-button>
|
||
</a-form>
|
||
</a-card>
|
||
|
||
<a-card title="租户管理" :bordered="false">
|
||
<a-table :data-source="tenants" row-key="companyId">
|
||
<a-table-column title="公司" data-index="name" />
|
||
<a-table-column title="套餐" data-index="planTier" />
|
||
<a-table-column title="老板账号" data-index="ownerAccount" />
|
||
<a-table-column title="账号数" data-index="accountCount" />
|
||
<a-table-column title="开放风险" data-index="openRiskCount" />
|
||
<a-table-column title="操作">
|
||
<template #default="{ record }">
|
||
<a-space>
|
||
<a-button size="small" @click="setCompanyStatus(record, 'active')">启用租户</a-button>
|
||
<a-button size="small" danger @click="setCompanyStatus(record, 'disabled')">停用租户</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<a-card title="开通预览" :bordered="false">
|
||
<a-descriptions bordered size="small" :column="1">
|
||
<a-descriptions-item v-for="item in insights.openingPreview" :key="text(item.label)" :label="text(item.label)">
|
||
{{ text(item.value) }}
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
</a-card>
|
||
|
||
<a-card title="交付检查" :bordered="false">
|
||
<div class="boss-admin-check-list">
|
||
<div v-for="item in insights.deliveryChecklist" :key="text(item.label)" class="boss-admin-check-row">
|
||
<span :class="{ done: Boolean(item.done) }">{{ Boolean(item.done) ? "✓" : "!" }}</span>
|
||
<strong>{{ text(item.label) }}</strong>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-customer-plans'" class="boss-admin-section-grid">
|
||
<a-card title="客户与套餐" :bordered="false">
|
||
<a-table :data-source="tenants" row-key="companyId">
|
||
<a-table-column title="企业" data-index="name" />
|
||
<a-table-column title="套餐" data-index="planTier" />
|
||
<a-table-column title="老板账号" data-index="ownerAccount" />
|
||
<a-table-column title="账号数" data-index="accountCount" />
|
||
<a-table-column title="设备数" data-index="deviceCount" />
|
||
<a-table-column title="合同到期" data-index="contractExpiresAt" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="套餐授权概览" :bordered="false">
|
||
<a-descriptions bordered size="small" :column="1">
|
||
<a-descriptions-item label="设备授权">{{ grants.devices.length }}</a-descriptions-item>
|
||
<a-descriptions-item label="项目授权">{{ grants.projects.length }}</a-descriptions-item>
|
||
<a-descriptions-item label="Skill 授权">{{ grants.skills.length }}</a-descriptions-item>
|
||
<a-descriptions-item label="Computer Use 在线">{{ insights.capabilitySummary.computerUseReady ?? 0 }}</a-descriptions-item>
|
||
<a-descriptions-item label="浏览器控制在线">{{ insights.capabilitySummary.browserReady ?? 0 }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-devices'" class="boss-admin-section-grid">
|
||
<a-card title="全局设备" :bordered="false">
|
||
<a-table :data-source="devices" row-key="id">
|
||
<a-table-column title="电脑" data-index="name" />
|
||
<a-table-column title="企业" data-index="companyName" />
|
||
<a-table-column title="账号" data-index="account" />
|
||
<a-table-column title="状态">
|
||
<template #default="{ record }">
|
||
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="GUI" data-index="codexGuiOnline" />
|
||
<a-table-column title="CLI" data-index="codexCliOnline" />
|
||
<a-table-column title="项目数" data-index="projectCount" />
|
||
<a-table-column title="风险" data-index="openRiskCount" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="能力分布" :bordered="false">
|
||
<div class="boss-admin-capability-grid">
|
||
<div>
|
||
<span>Codex GUI</span>
|
||
<strong>{{ insights.capabilitySummary.guiReady ?? 0 }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>Codex CLI</span>
|
||
<strong>{{ insights.capabilitySummary.cliReady ?? 0 }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>Computer Use</span>
|
||
<strong>{{ insights.capabilitySummary.computerUseReady ?? 0 }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>Browser</span>
|
||
<strong>{{ insights.capabilitySummary.browserReady ?? 0 }}</strong>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-customer-success'" class="boss-admin-section-grid">
|
||
<a-card title="客户健康列表" :bordered="false">
|
||
<a-table :data-source="insights.customerHealthRows" row-key="companyId">
|
||
<a-table-column title="企业" data-index="name" />
|
||
<a-table-column title="健康分" data-index="healthScore" />
|
||
<a-table-column title="套餐" data-index="planTier" />
|
||
<a-table-column title="在线设备" data-index="onlineDevices" />
|
||
<a-table-column title="当前风险" data-index="openRiskCount" />
|
||
<a-table-column title="负责人" data-index="ownerAccount" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="风险聚合" :bordered="false">
|
||
<div class="boss-admin-status-list">
|
||
<div v-for="item in insights.riskAggregates" :key="text(item.label)" class="boss-admin-status-row">
|
||
<span>{{ text(item.label) }}</span>
|
||
<a-tag :color="numberValue(item.value) > 0 ? 'orange' : 'green'">{{ text(item.value) }}</a-tag>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
<a-card title="客户成功跟进记录" :bordered="false">
|
||
<a-list :data-source="insights.customerFollowups">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta :title="text(item.label)" :description="text(item.meta)" />
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
<a-card title="风险通知" :bordered="false">
|
||
<a-table :data-source="notifications" row-key="notificationId">
|
||
<a-table-column title="通知" data-index="title" />
|
||
<a-table-column title="级别" data-index="severity" />
|
||
<a-table-column title="公司" data-index="companyId" />
|
||
<a-table-column title="时间" data-index="createdAt" />
|
||
</a-table>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-risk'" class="boss-admin-section">
|
||
<a-card title="全局风险处置" :bordered="false">
|
||
<div class="boss-admin-action-strip">
|
||
<a-input v-model:value="riskForm.ownerAccount" placeholder="负责人账号,用于指派负责人" />
|
||
<a-input v-model:value="riskForm.slaDueAt" placeholder="SLA 时间,如 2026-05-02T18:00:00+08:00" />
|
||
<a-input v-model:value="riskForm.note" placeholder="处理备注" />
|
||
</div>
|
||
<a-table :data-source="risks" row-key="riskId">
|
||
<a-table-column title="风险" data-index="title" />
|
||
<a-table-column title="级别">
|
||
<template #default="{ record }">
|
||
<a-tag :color="riskColor(record.severity)">{{ text(record.severity) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="公司" data-index="companyId" />
|
||
<a-table-column title="负责人" data-index="ownerAccount" />
|
||
<a-table-column title="SLA" data-index="slaDueAt" />
|
||
<a-table-column title="操作">
|
||
<template #default="{ record }">
|
||
<a-space wrap>
|
||
<a-button size="small" @click="handleRisk(record, 'assign_owner')">指派负责人</a-button>
|
||
<a-button size="small" @click="handleRisk(record, 'set_sla')">设置 SLA</a-button>
|
||
<a-button size="small" @click="handleRisk(record, 'ack')">确认风险</a-button>
|
||
<a-button size="small" danger @click="handleRisk(record, 'resolve')">关闭风险</a-button>
|
||
<a-button size="small" @click="handleRisk(record, 'create_repair_ticket')">创建工单</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-audit'" class="boss-admin-section-grid">
|
||
<a-card title="系统审计" :bordered="false">
|
||
<a-table :data-source="auditLogs" row-key="auditId">
|
||
<a-table-column title="操作人" data-index="actorAccount" />
|
||
<a-table-column title="动作" data-index="action" />
|
||
<a-table-column title="对象账号" data-index="targetAccount" />
|
||
<a-table-column title="详情" data-index="detail" />
|
||
<a-table-column title="时间" data-index="createdAt" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="风险时间线" :bordered="false">
|
||
<a-table :data-source="riskTimeline" row-key="eventId">
|
||
<a-table-column title="风险" data-index="riskId" />
|
||
<a-table-column title="动作" data-index="action" />
|
||
<a-table-column title="操作人" data-index="actorAccount" />
|
||
<a-table-column title="时间" data-index="createdAt" />
|
||
</a-table>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'platform-billing'" class="boss-admin-section-grid">
|
||
<a-card title="计费与授权" :bordered="false">
|
||
<a-table :data-source="tenants" row-key="companyId">
|
||
<a-table-column title="公司" data-index="name" />
|
||
<a-table-column title="套餐" data-index="planTier" />
|
||
<a-table-column title="状态" data-index="lifecycleStatus" />
|
||
<a-table-column title="合同到期" data-index="contractExpiresAt" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="授权概览" :bordered="false">
|
||
<a-descriptions bordered size="small" :column="1">
|
||
<a-descriptions-item label="设备授权">{{ grants.devices.length }}</a-descriptions-item>
|
||
<a-descriptions-item label="项目授权">{{ grants.projects.length }}</a-descriptions-item>
|
||
<a-descriptions-item label="Skill 授权">{{ grants.skills.length }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-overview'" class="boss-admin-section-grid">
|
||
<a-card class="boss-admin-hero" :bordered="false">
|
||
<p class="boss-admin-eyebrow">企业总览 · {{ currentCompanyName }}</p>
|
||
<h3>本企业成员、电脑、项目和风险的实时工作台</h3>
|
||
<div class="boss-admin-metrics">
|
||
<div v-for="metric in enterpriseMetricCards" :key="metric.label" class="boss-admin-metric" :class="metric.tone">
|
||
<span>{{ metric.label }}</span>
|
||
<strong>{{ metric.value }}</strong>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="经营目标" :bordered="false">
|
||
<div class="boss-admin-goal-list">
|
||
<div v-for="goal in insights.enterpriseGoals" :key="text(goal.projectId)" class="boss-admin-goal-row">
|
||
<div>
|
||
<strong>{{ text(goal.label) }}</strong>
|
||
<span>{{ text(goal.progress) }}%</span>
|
||
</div>
|
||
<a-progress :percent="numberValue(goal.progress)" :show-info="false" />
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="部门进展" :bordered="false">
|
||
<div class="boss-admin-status-list">
|
||
<div v-for="item in insights.departmentProgress" :key="text(item.label)" class="boss-admin-status-row">
|
||
<span>{{ text(item.label) }}</span>
|
||
<a-tag :color="text(item.tone, 'green')">{{ text(item.note) }}</a-tag>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="主 Agent 摘要" :bordered="false">
|
||
<a-list :data-source="insights.masterAgentSummary">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>{{ item }}</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
|
||
<a-card title="项目进展" :bordered="false">
|
||
<a-table :data-source="projects.slice(0, 8)" row-key="id">
|
||
<a-table-column title="项目" data-index="name" />
|
||
<a-table-column title="文件夹" data-index="folderName" />
|
||
<a-table-column title="模式" data-index="collaborationMode" />
|
||
<a-table-column title="更新时间" data-index="updatedAt" />
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<a-card title="企业风险" :bordered="false">
|
||
<a-table :data-source="risks.slice(0, 6)" row-key="riskId">
|
||
<a-table-column title="风险" data-index="title" />
|
||
<a-table-column title="对象" data-index="deviceId" />
|
||
<a-table-column title="级别" data-index="severity" />
|
||
<a-table-column title="时间" data-index="lastSeenAt" />
|
||
</a-table>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-members'" class="boss-admin-section-grid">
|
||
<a-card title="新建账号" :bordered="false">
|
||
<a-form layout="vertical" class="boss-admin-form">
|
||
<a-form-item label="账号">
|
||
<a-input v-model:value="accountForm.account" placeholder="member@example.com" />
|
||
</a-form-item>
|
||
<a-form-item label="昵称">
|
||
<a-input v-model:value="accountForm.displayName" placeholder="成员姓名" />
|
||
</a-form-item>
|
||
<a-form-item label="角色">
|
||
<a-select v-model:value="accountForm.role">
|
||
<a-select-option value="member">member</a-select-option>
|
||
<a-select-option value="admin">admin</a-select-option>
|
||
<a-select-option value="highest_admin">highest_admin</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="所属企业">
|
||
<a-select v-model:value="accountForm.companyId" allow-clear>
|
||
<a-select-option v-for="company in tenants" :key="text(company.companyId)" :value="text(company.companyId)">
|
||
{{ text(company.name) }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="初始密码">
|
||
<a-input-password v-model:value="accountForm.password" placeholder="留空时只更新资料" />
|
||
</a-form-item>
|
||
<a-button type="primary" block :loading="mutating" @click="saveAccount">新建账号</a-button>
|
||
</a-form>
|
||
</a-card>
|
||
|
||
<a-card title="组织与成员" :bordered="false">
|
||
<a-table :data-source="users" row-key="id">
|
||
<a-table-column title="账号" data-index="account" />
|
||
<a-table-column title="昵称" data-index="displayName" />
|
||
<a-table-column title="角色" data-index="role" />
|
||
<a-table-column title="状态">
|
||
<template #default="{ record }">
|
||
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="操作">
|
||
<template #default="{ record }">
|
||
<a-space wrap>
|
||
<a-button size="small" @click="setAccountStatus(record, 'active')">启用账号</a-button>
|
||
<a-button size="small" @click="resetPassword(record)">重置密码</a-button>
|
||
<a-button size="small" danger @click="setAccountStatus(record, 'disabled')">停用账号</a-button>
|
||
<a-button size="small" danger @click="reclaimAccount(record)">离职回收</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<a-card title="组织结构" :bordered="false">
|
||
<div class="boss-admin-org-grid">
|
||
<div v-for="unit in insights.organizationUnits" :key="unit" class="boss-admin-org-node">
|
||
{{ unit }}
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="权限详情" :bordered="false">
|
||
<div class="boss-admin-permission-list">
|
||
<a-tag v-for="permission in insights.permissionHighlights" :key="permission" color="green">
|
||
{{ permission }}
|
||
</a-tag>
|
||
</div>
|
||
</a-card>
|
||
|
||
<a-card title="角色权限" :bordered="false">
|
||
<a-list :data-source="roles">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta :title="text(item.label)" :description="text(item.description)" />
|
||
<a-tag>{{ item.role }}</a-tag>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-devices-agents'" class="boss-admin-section-grid">
|
||
<a-card title="电脑与 Codex 接入" :bordered="false">
|
||
<a-table :data-source="devices" row-key="id">
|
||
<a-table-column title="电脑" data-index="name" />
|
||
<a-table-column title="状态">
|
||
<template #default="{ record }">
|
||
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="控制模式" data-index="preferredExecutionMode" />
|
||
<a-table-column title="最近心跳" data-index="lastSeenAt" />
|
||
<a-table-column title="风险" data-index="openRiskCount" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="项目线程" :bordered="false">
|
||
<a-table :data-source="projects" row-key="id">
|
||
<a-table-column title="项目" data-index="name" />
|
||
<a-table-column title="线程" data-index="threadDisplayName" />
|
||
<a-table-column title="未读" data-index="unreadCount" />
|
||
<a-table-column title="风险" data-index="riskLevel" />
|
||
<a-table-column title="更新时间" data-index="updatedAt" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="Agent 流程" :bordered="false">
|
||
<div class="boss-admin-flow">
|
||
<template v-for="(step, index) in insights.agentFlowSteps" :key="step">
|
||
<div class="boss-admin-flow-node">{{ step }}</div>
|
||
<div v-if="index < insights.agentFlowSteps.length - 1" class="boss-admin-flow-arrow">→</div>
|
||
</template>
|
||
</div>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-agent-flows'" class="boss-admin-section-grid">
|
||
<a-card title="Agent 与流程" :bordered="false">
|
||
<a-list :data-source="projects">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta
|
||
:title="text(item.name)"
|
||
:description="`当前模式:${text(item.collaborationMode)} · 最近更新:${text(item.updatedAt)}`"
|
||
/>
|
||
<a-tag :color="text(item.riskLevel) === 'low' ? 'green' : 'orange'">{{ text(item.riskLevel) }}</a-tag>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
<a-card title="权限模板" :bordered="false">
|
||
<a-list :data-source="templates">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta :title="text(item.name)" :description="text(item.description)" />
|
||
<a-tag color="green">{{ item.templateId }}</a-tag>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-skill'" class="boss-admin-section-grid">
|
||
<a-card title="创建 Skill 请求" :bordered="false">
|
||
<a-form layout="vertical" class="boss-admin-form">
|
||
<a-form-item label="动作">
|
||
<a-select v-model:value="skillRequestForm.action">
|
||
<a-select-option value="install">install</a-select-option>
|
||
<a-select-option value="update">update</a-select-option>
|
||
<a-select-option value="uninstall">uninstall</a-select-option>
|
||
<a-select-option value="rollback">rollback</a-select-option>
|
||
<a-select-option value="version_lock">version_lock</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="设备">
|
||
<a-select v-model:value="skillRequestForm.deviceId" show-search>
|
||
<a-select-option v-for="device in devices" :key="text(device.id)" :value="text(device.id)">
|
||
{{ text(device.name) }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="Skill">
|
||
<a-select v-model:value="skillRequestForm.skillId" allow-clear show-search>
|
||
<a-select-option v-for="skill in skills" :key="text(skill.skillId)" :value="text(skill.skillId)">
|
||
{{ text(skill.name) }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="来源 URL">
|
||
<a-input v-model:value="skillRequestForm.sourceUrl" placeholder="安装或更新远端来源" />
|
||
</a-form-item>
|
||
<a-form-item label="版本 / 回滚 / 锁定">
|
||
<a-input-group compact>
|
||
<a-input v-model:value="skillRequestForm.targetVersion" style="width: 33%" placeholder="targetVersion" />
|
||
<a-input v-model:value="skillRequestForm.rollbackToVersion" style="width: 33%" placeholder="rollbackToVersion" />
|
||
<a-input v-model:value="skillRequestForm.lockedVersion" style="width: 34%" placeholder="lockedVersion" />
|
||
</a-input-group>
|
||
</a-form-item>
|
||
<a-form-item label="校验和 / 备注">
|
||
<a-input v-model:value="skillRequestForm.checksum" placeholder="sha256 checksum" />
|
||
<a-input v-model:value="skillRequestForm.note" class="boss-admin-form-gap" placeholder="备注" />
|
||
</a-form-item>
|
||
<a-button type="primary" block :loading="mutating" @click="createSkillRequest">创建 Skill 请求</a-button>
|
||
</a-form>
|
||
</a-card>
|
||
|
||
<a-card title="Skill 中心" :bordered="false">
|
||
<a-table :data-source="skills" row-key="skillId">
|
||
<a-table-column title="Skill" data-index="name" />
|
||
<a-table-column title="说明" data-index="description" />
|
||
<a-table-column title="分类" data-index="category" />
|
||
<a-table-column title="设备数" data-index="deviceCount" />
|
||
<a-table-column title="更新时间" data-index="updatedAt" />
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="使用审计" :bordered="false">
|
||
<a-list :data-source="insights.skillUsageAudit">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item>
|
||
<a-list-item-meta :title="text(item.label)" :description="text(item.meta)" />
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-risk-backup'" class="boss-admin-section-grid">
|
||
<a-card title="风险与审计" :bordered="false">
|
||
<div class="boss-admin-action-strip">
|
||
<a-input v-model:value="riskForm.ownerAccount" placeholder="负责人账号" />
|
||
<a-input v-model:value="riskForm.slaDueAt" placeholder="SLA 时间" />
|
||
<a-input v-model:value="riskForm.note" placeholder="处理备注" />
|
||
</div>
|
||
<a-table :data-source="risks" row-key="riskId">
|
||
<a-table-column title="风险" data-index="title" />
|
||
<a-table-column title="对象" data-index="deviceId" />
|
||
<a-table-column title="级别" data-index="severity" />
|
||
<a-table-column title="操作">
|
||
<template #default="{ record }">
|
||
<a-space wrap>
|
||
<a-button size="small" @click="handleRisk(record, 'assign_owner')">指派负责人</a-button>
|
||
<a-button size="small" @click="handleRisk(record, 'set_sla')">设置 SLA</a-button>
|
||
<a-button size="small" @click="handleRisk(record, 'ack')">确认风险</a-button>
|
||
<a-button size="small" danger @click="handleRisk(record, 'resolve')">关闭风险</a-button>
|
||
<a-button size="small" @click="handleRisk(record, 'create_repair_ticket')">创建工单</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="业务级回退" :bordered="false">
|
||
<div class="boss-admin-recovery-grid">
|
||
<div v-for="action in insights.recoveryActions" :key="action" class="boss-admin-recovery-card">
|
||
{{ action }}
|
||
</div>
|
||
</div>
|
||
<a-descriptions class="boss-admin-backup-status" bordered size="small" :column="1">
|
||
<a-descriptions-item label="最近备份">{{ text(effectiveBackupStatus.lastBackupAt) }}</a-descriptions-item>
|
||
<a-descriptions-item label="校验状态">{{ text(effectiveBackupStatus.status) }}</a-descriptions-item>
|
||
<a-descriptions-item label="可回退点">{{ text(effectiveBackupStatus.restorePointCount) }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
</a-card>
|
||
<a-card title="授权清单" :bordered="false">
|
||
<a-table :data-source="grantRows" row-key="grantId">
|
||
<a-table-column title="范围" data-index="scopeLabel" />
|
||
<a-table-column title="账号" data-index="account" />
|
||
<a-table-column title="目标" data-index="targetLabel" />
|
||
<a-table-column title="权限">
|
||
<template #default="{ record }">
|
||
{{ permissionText(record.permissions) }}
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="操作">
|
||
<template #default="{ record }">
|
||
<a-button size="small" danger @click="revokeGrant(record)">撤销授权</a-button>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else-if="activeKey === 'enterprise-backup'" class="boss-admin-section-grid">
|
||
<a-card title="备份与回退" :bordered="false">
|
||
<div class="boss-admin-recovery-grid">
|
||
<div v-for="action in insights.recoveryActions" :key="action" class="boss-admin-recovery-card">
|
||
{{ action }}
|
||
</div>
|
||
</div>
|
||
<a-descriptions class="boss-admin-backup-status" bordered size="small" :column="1">
|
||
<a-descriptions-item label="最近备份">{{ text(effectiveBackupStatus.lastBackupAt) }}</a-descriptions-item>
|
||
<a-descriptions-item label="校验状态">{{ text(effectiveBackupStatus.status) }}</a-descriptions-item>
|
||
<a-descriptions-item label="可回退点">{{ text(effectiveBackupStatus.restorePointCount) }}</a-descriptions-item>
|
||
<a-descriptions-item label="备份目录">{{ text(effectiveBackupStatus.backupDir) }}</a-descriptions-item>
|
||
</a-descriptions>
|
||
<div class="boss-admin-action-strip boss-admin-form-gap">
|
||
<a-input v-model:value="backupReason" placeholder="快照备注,例如 release-before-upgrade" />
|
||
<a-button type="primary" :loading="mutating || backupLoading" @click="createStateSnapshot">
|
||
创建状态快照
|
||
</a-button>
|
||
<a-button :loading="backupLoading" @click="loadBackupSnapshots">刷新快照</a-button>
|
||
</div>
|
||
</a-card>
|
||
<a-card title="快照清单" :bordered="false">
|
||
<a-table :loading="backupLoading" :data-source="backupSnapshots" row-key="snapshotId">
|
||
<a-table-column title="快照" data-index="snapshotId" />
|
||
<a-table-column title="创建时间" data-index="createdAt" />
|
||
<a-table-column title="创建人" data-index="actorAccount" />
|
||
<a-table-column title="备注" data-index="reason" />
|
||
<a-table-column title="大小">
|
||
<template #default="{ record }">
|
||
{{ formatBytes(record.bytes) }}
|
||
</template>
|
||
</a-table-column>
|
||
<a-table-column title="操作">
|
||
<template #default="{ record }">
|
||
<a-button size="small" danger :loading="mutating" @click="restoreStateSnapshot(record)">
|
||
恢复到此快照
|
||
</a-button>
|
||
</template>
|
||
</a-table-column>
|
||
</a-table>
|
||
</a-card>
|
||
<a-card title="资源授权" :bordered="false">
|
||
<a-form layout="vertical" class="boss-admin-form">
|
||
<a-form-item label="账号">
|
||
<a-select v-model:value="grantForm.account" show-search>
|
||
<a-select-option v-for="user in users" :key="text(user.account)" :value="text(user.account)">
|
||
{{ text(user.displayName) }} · {{ text(user.account) }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="授权范围">
|
||
<a-segmented v-model:value="grantForm.scope" :options="['device', 'project', 'skill']" />
|
||
</a-form-item>
|
||
<a-form-item label="目标">
|
||
<a-select v-model:value="grantForm.targetId" show-search>
|
||
<a-select-option v-for="option in grantTargetOptions" :key="option.id" :value="option.id">
|
||
{{ option.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="权限模板">
|
||
<a-select v-model:value="grantForm.templateId">
|
||
<a-select-option v-for="template in templates" :key="text(template.templateId)" :value="text(template.templateId)">
|
||
{{ text(template.name) }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="权限">
|
||
<a-select v-model:value="grantForm.permissions" mode="tags" :placeholder="selectedScopePermissionPlaceholder" />
|
||
</a-form-item>
|
||
<a-space>
|
||
<a-button type="primary" :loading="mutating" @click="submitGrant">分配资源</a-button>
|
||
<a-button :loading="mutating" @click="applyPermissionTemplate">套用权限模板</a-button>
|
||
</a-space>
|
||
</a-form>
|
||
</a-card>
|
||
</section>
|
||
|
||
<section v-else class="boss-admin-section">
|
||
<a-card :title="adminSurface === 'platform' ? '平台设置' : '企业设置'" :bordered="false">
|
||
<p>当前独立后台已接入 Boss Admin BFF,并已具备租户、账号、授权、风险和 Skill 的基础治理动作。</p>
|
||
<a-descriptions bordered size="small" :column="1">
|
||
<a-descriptions-item v-for="(value, key) in payload?.yudaoMapping ?? {}" :key="key" :label="key">
|
||
{{ value }}
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
</a-card>
|
||
</section>
|
||
</a-spin>
|
||
</main>
|
||
</div>
|
||
</a-config-provider>
|
||
</template>
|