代码修改
This commit is contained in:
1 parent
030ede6887
commit
151384b26d
3 files changed
+67
-16
No files matched your search
@@ -90,7 +90,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
||||
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine truncated
|
||||
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
|
||||
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(2.5) **`?hideChrome=1`** — **仅**去全局 chrome(Header 导航栏 + PageSidebar 侧边栏),**不**强制主题、**不**隔离鉴权(区别于 `embed`:只想要一个干净全屏页面,如「大屏绘制」`/page/strategy/dashboard-agent?taskId=…&hideChrome=1`)。新增 `EmbedSession.hideChrome` 字段 + `readHideChromeParam()` + `isChromeHidden()`(取 `embed || hideChrome` 之并),与 `hideJump` 完全对称;`PageRoutes.tsx` 的 `PageLayout` 把 `chromeHidden`(`isChromeHidden() || readHideChromeParam(location.search)==="on"`)并入 `pageSidebarAllowed`,且其**优先级高于** dashboard-agent 路由原本的强制全局 chrome(`forceChromeForDashboardAgent`)。`hideChrome=0` 可显式关闭。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
|
||||
- **操作日志(Y-Log)上报** — 用户的关键操作(登录登出/查询/查看/增删改/启停/导入导出/问答等)成功后,**前端直连**外部接口 `POST {write_url}`(默认 `https://ch1.b.uat.4.cn/api/y-log/write`)记一条审计日志。核心模块 `src/core/log/`:`y-log.ts`(`writeYLog(params)` + `OptType` 枚举 0/1/3..12;**异步即发即忘、永不 await、永不抛错、失败静默**,自动把完整 url 转相对路径去 ip/host、对象入参/返回自动 `JSON.stringify` + 50k 裁剪、`optResult` 恒 0 只报成功),`y-log-config.ts`(从公开只读 `GET /api/public/config/y-log` 拉 `{enabled, writeUrl}`,模块级缓存 + 去重 + 默认兜底;`App.tsx` `AppProviders` 启动预热 `fetchYLogConfig()`),`y-log-token.ts`(token1 = token 登录的 `?authToken=`,经 [[scoped-storage]] 的 `authStorage*` 存取,嵌入态自动隔离)。**双门控**:仅当①存在 token1(即用 token 登录,`LoginPage` 成功后 `persistYLogToken(authToken)`,登出 `account-settings-page.tsx` 先报后清 token1)且②配置 `enabled && writeUrl` 才上报;否则一律不发。`optDetail` 统一「B1-CM助手-…」(**禁内部框架名**)。埋点**广覆盖**各 API client 的成功路径——原则是「一个用户操作(查看/查询/新增/编辑/删除/启停/导入导出)= 一条日志」,每个 API client 的**单条详情 GET(查看→view 4)/ 列表查询 GET(search 3)/ 新增 POST(create 9)/ 改 PUT·PATCH(edit 5)/ 删 DELETE(delete 6)/ 启停(enabled 11 / disabled 10)/ 导入导出(import 7 / export 8)**都各落一条,仅跳过高频 typeahead/状态轮询/纯 bootstrap config 读。覆盖文件:`strategy-components/api/{sensitive-words,menu-overrides,task-buttons,skill-addresses,light-apps,business-mapping}.ts`、`roundtable-planning/api/{drafts,task-drafts,roundtable-jobs,recommend,chains,multi-agent,dashboard-sessions}.ts`、`core/{agents,scheduled-tasks,knowledge,positions,tags,studio,system-settings,memory,mcp,models,custom-prompts,user-preferences,notifications,recommended-questions,user-qa,curator,admin,thread-shares,roundtable-draft-shares,tool-metrics,llm-metrics,note,artifacts}/api.ts`、`core/api/markdown-docx-export.ts`。**问答(Q&A)改为「一次大模型调用 = 一条日志」**:在共享的 `core/threads/hooks.ts` `useThreadStream` 的 `onLangChainEvent` 里监听 `on_chat_model_end`,每次模型调用结束就上报一条(optType 12,`reqParam` 取本次 input 的最后一条 user 文本 + 模型名,`returnParam` 取本次输出文本)——一个问答轮次里 lead agent / 子代理的**每次** LLM 调用都各记一条,覆盖所有走 `useThreadStream` 的页面(`ChatPage`/`AgentChatPage`/创建智能体/iframe/会商等);`ChatPage`/`AgentChatPage` 的 `onFinish` 仍额外保留一条轮次汇总。后端仅新增公开只读配置:`deerflow/config/y_log_config.py`(`YLogConfig{enabled,write_url}`)挂到 `AppConfig.y_log`,`app/gateway/routers/public_config.py` 加 `GET /api/public/config/y-log`,地址写在 `config.yaml` 的 `y_log` 段(mtime 热重载,前端无需重建);**不做后端代理**。Tests:`tests/test_y_log_config.py`。
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -230,23 +230,32 @@ function DisposalReal({ data }: { data: DisposalData }) {
|
||||
key={i}
|
||||
className="rounded-md border border-slate-200 bg-slate-50/60 p-4 dark:border-[var(--dash-border)] dark:bg-[var(--dash-elev)]"
|
||||
>
|
||||
{/* 标题行:序号 + 建议内容 + 口径基调 */}
|
||||
<div className="mb-3 flex items-start gap-2.5">
|
||||
{/* 标题行:序号 + 建议内容(基调移到下方独立成行,长文不再挤压标题) */}
|
||||
<div className="flex items-start gap-2.5">
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-600 text-[11px] font-bold text-white">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 text-xs font-semibold leading-relaxed text-slate-800 dark:text-[var(--dash-text-strong)]">
|
||||
{d.content || "—"}
|
||||
</div>
|
||||
{d.tone && (
|
||||
<span className="shrink-0">
|
||||
<Tag tone="amber">{d.tone}</Tag>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 基调:拆成关键词小药丸,整行自适应换行——长基调不再被压成右侧一长条。
|
||||
与序号对齐缩进(序号 20px + gap 10px = 30px)。 */}
|
||||
{d.tone && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5 pl-[30px]">
|
||||
<span className="text-[11px] font-semibold text-slate-400 dark:text-[var(--dash-text-dim)]">
|
||||
基调
|
||||
</span>
|
||||
{splitTone(d.tone).map((t, ti) => (
|
||||
<Tag key={ti} tone="amber">
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 其余字段:带标签两列网格 */}
|
||||
{(d.method || d.effect || d.subject || d.basis) && (
|
||||
<dl className="grid grid-cols-1 gap-x-6 gap-y-2.5 border-t border-dashed border-slate-200 pt-3 dark:border-[var(--dash-border)] sm:grid-cols-2">
|
||||
<dl className="mt-3 grid grid-cols-1 gap-x-6 gap-y-2.5 border-t border-dashed border-slate-200 pt-3 dark:border-[var(--dash-border)] sm:grid-cols-2">
|
||||
{d.method && <Field label="处置方式与步骤" value={d.method} />}
|
||||
{d.effect && <Field label="预期效果" value={d.effect} />}
|
||||
{d.subject && <Field label="主责单位" value={d.subject} />}
|
||||
@@ -262,6 +271,21 @@ function DisposalReal({ data }: { data: DisposalData }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把「基调」串按常见分隔符(顿号 / 逗号 / 斜杠 / 分号 / 竖线 / 空白)拆成关键词数组,去空去重。
|
||||
* 整串无分隔符(单个词或一句话)则原样返回单个元素——既支持「悲愤、追责、人道主义审判」拆成
|
||||
* 三枚药丸,也兼容只有一个基调词的情况。
|
||||
*/
|
||||
function splitTone(raw: string): string[] {
|
||||
const parts = raw
|
||||
.split(/[、,,//;;||\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const seen = new Set<string>();
|
||||
const uniq = parts.filter((p) => (seen.has(p) ? false : (seen.add(p), true)));
|
||||
return uniq.length > 0 ? uniq : [raw.trim()];
|
||||
}
|
||||
|
||||
/** 处置建议卡片里的「标签 + 值」字段(标签红色小字,值正文)。 */
|
||||
function Field({ label, value }: { label: string; value: string }) {
|
||||
const ds = useDesensitize();
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation, useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2, RefreshCw, Sparkles } from "lucide-react";
|
||||
import { Inbox, Loader2, RefreshCw, Sparkles } from "lucide-react";
|
||||
|
||||
import { fetchDashboardInputJson } from "@/core/auth/dashboard-input";
|
||||
import { readCopTaskDetail } from "@/core/auth/task-deeplink";
|
||||
@@ -77,6 +77,16 @@ function deepDesensitize<T>(value: T, fn: (s: string) => string): T {
|
||||
type DashView = "chat" | "page";
|
||||
type RenderMode = "app" | "html";
|
||||
|
||||
/**
|
||||
* 战情报告是否含可展示内容。`parseSqReport` **永远**返回非 null 的 SqReport 对象(各类目可为 null),
|
||||
* 故判空必须看「是否至少有一个类目非空」,不能用 `!!report`——否则全类目皆空时仍误判为有数据,
|
||||
* 进而渲染出 DEMO 兜底的大屏标题 + 空导航 Tab。全空 → 整页走空状态(不渲染大屏标题/导航/页脚)。
|
||||
*/
|
||||
function sqReportHasContent(r: SqReport | null): boolean {
|
||||
if (!r) return false;
|
||||
return !!(r.enemy || r.ours || r.environment || r.judgement || r.decision || r.disposal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态页面阶段开关(先按设计图搭总览版式,动态数据后续再接):
|
||||
* - 默认落在「大屏页面」视图,直接展示 SituationOverview 静态版式;
|
||||
@@ -284,9 +294,10 @@ export default function DashboardAgentPage() {
|
||||
}
|
||||
}, [sqReport, desensitizeText]);
|
||||
|
||||
// 是否有可展示的真实数据:战情报告(safeReport)或已生成的大屏(report-json / HTML)。
|
||||
// 三者皆无 → 不渲染大屏的导航栏与标题,仅显示「暂无数据」。
|
||||
const hasData = !!safeReport || !!dashboardData || !!dashboardHtml;
|
||||
// 是否有可展示的真实数据:战情报告**至少一个类目非空**(safeReport 恒为非 null 对象,不能用
|
||||
// `!!safeReport` 判空,见 sqReportHasContent),或已生成的大屏(report-json / HTML)。
|
||||
// 三者皆无 → 不渲染大屏的导航栏与标题,改显示整页空状态。
|
||||
const hasData = sqReportHasContent(safeReport) || !!dashboardData || !!dashboardHtml;
|
||||
|
||||
// 战情多页:总览 + 六个详情页;详情页传入对应的(脱敏后)真实数据(无则页面回退占位)。
|
||||
// 导航 Tab 文本(label)也经 desensitizeText 脱敏后再传给 DashboardApp 渲染导航栏。
|
||||
@@ -410,9 +421,25 @@ export default function DashboardAgentPage() {
|
||||
</div>
|
||||
) : view === "page" ? (
|
||||
!hasData ? (
|
||||
// 无数据:隐藏大屏导航栏与顶部标题,仅显示「暂无数据」。
|
||||
<div className="flex flex-1 items-center justify-center text-sm text-slate-400">
|
||||
暂无数据
|
||||
// 无数据:不渲染大屏标题/导航 Tab/页脚,整页改为一个干净的空状态(图标 + 文案 + 重试)。
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-6 py-16 text-center">
|
||||
<div className="relative mb-5">
|
||||
<div className="absolute inset-0 -z-10 rounded-full bg-rose-100/50 blur-2xl dark:bg-rose-500/10" />
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-2xl border border-rose-100 bg-gradient-to-b from-rose-50 to-white text-rose-500 shadow-sm dark:border-[var(--dash-border)] dark:from-[var(--dash-card)] dark:to-[var(--dash-page)] dark:text-[var(--dash-primary)]">
|
||||
<Inbox className="h-9 w-9" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-1.5 text-base font-bold text-slate-700 dark:text-[var(--dash-text-strong)]">
|
||||
暂无大屏数据
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void initialize()}
|
||||
className="inline-flex items-center gap-1.5 rounded-xl border border-slate-200 bg-white px-4 py-2 text-xs font-bold text-slate-600 shadow-sm transition hover:border-rose-200 hover:text-rose-600 dark:border-[var(--dash-border)] dark:bg-[var(--dash-card)] dark:text-[var(--dash-text)] dark:hover:text-[var(--dash-primary)]"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
重新请求
|
||||
</button>
|
||||
</div>
|
||||
) : renderMode === "html" && htmlBlobUrl ? (
|
||||
<iframe
|
||||
|
||||
Reference in new issue
Block a user