From 681abeed45eb058cfd8fa61258006143e1930cd8 Mon Sep 17 00:00:00 2001 From: ayou <550244300@qq.com> Date: Mon, 22 Sep 2025 22:15:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=8F=8A=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增左上角流程信息展示组件,显示流程编码和名称 后端 FlowDoc 结构增加 name/code/remark 字段支持 添加从 YAML 提取名称的兜底逻辑 --- backend/src/services/flow_service.rs | 35 ++++++++-- frontend/src/flows/components/tools/index.tsx | 67 ++++++++++++++++--- .../src/flows/components/tools/styles.tsx | 47 +++++++++++++ 3 files changed, 131 insertions(+), 18 deletions(-) diff --git a/backend/src/services/flow_service.rs b/backend/src/services/flow_service.rs index 77aed00..630ff89 100644 --- a/backend/src/services/flow_service.rs +++ b/backend/src/services/flow_service.rs @@ -26,7 +26,14 @@ pub struct FlowSummary { #[serde(skip_serializing_if = "Option::is_none")] pub last_modified_by: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FlowDoc { pub id: String, pub yaml: String, #[serde(skip_serializing_if = "Option::is_none")] pub design_json: Option } +pub struct FlowDoc { + pub id: String, + pub yaml: String, + #[serde(skip_serializing_if = "Option::is_none")] pub design_json: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub remark: Option, +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FlowCreateReq { pub yaml: Option, pub name: Option, pub design_json: Option, pub code: Option, pub remark: Option } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -98,9 +105,13 @@ pub async fn create(db: &Db, req: FlowCreateReq) -> anyhow::Result { .or_else(|| req.yaml.as_deref().and_then(extract_name)); let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()); let design_json_str = match &req.design_json { Some(v) => serde_json::to_string(v).ok(), None => None }; + // 克隆一份用于返回 + let ret_name = name.clone(); + let ret_code = req.code.clone(); + let ret_remark = req.remark.clone(); let am = db_flow::ActiveModel { id: Set(id.clone()), - name: Set(name), + name: Set(name.clone()), yaml: Set(req.yaml.clone()), design_json: Set(design_json_str), // 新增: code 与 remark 入库 @@ -115,7 +126,7 @@ pub async fn create(db: &Db, req: FlowCreateReq) -> anyhow::Result { match db_flow::Entity::insert(am).exec(db).await { Ok(_) => { info!(target: "udmin", "flow.create: insert ok id={}", id); - Ok(FlowDoc { id, yaml: req.yaml.unwrap_or_default(), design_json: req.design_json }) + Ok(FlowDoc { id, yaml: req.yaml.unwrap_or_default(), design_json: req.design_json, name: ret_name, code: ret_code, remark: ret_remark }) } Err(DbErr::RecordNotInserted) => { // Workaround for MySQL + non-auto-increment PK: verify by reading back @@ -123,7 +134,7 @@ pub async fn create(db: &Db, req: FlowCreateReq) -> anyhow::Result { match db_flow::Entity::find_by_id(id.clone()).one(db).await { Ok(Some(_)) => { info!(target: "udmin", "flow.create: found inserted row by id={}, treating as success", id); - Ok(FlowDoc { id, yaml: req.yaml.unwrap_or_default(), design_json: req.design_json }) + Ok(FlowDoc { id, yaml: req.yaml.unwrap_or_default(), design_json: req.design_json, name, code: req.code, remark: req.remark }) } Ok(None) => Err(anyhow::anyhow!("insert flow failed").context("verify inserted row not found")), Err(e) => Err(anyhow::Error::new(e).context("insert flow failed")), @@ -141,7 +152,12 @@ pub async fn get(db: &Db, id: &str) -> anyhow::Result { let row = row.ok_or_else(|| anyhow::anyhow!("not found"))?; let yaml = row.yaml.unwrap_or_default(); let design_json = row.design_json.and_then(|s| serde_json::from_str::(&s).ok()); - Ok(FlowDoc { id: row.id, yaml, design_json }) + // 名称兜底:数据库 name 为空时,尝试从 YAML 提取 + let name = row + .name + .clone() + .or_else(|| extract_name(&yaml)); + Ok(FlowDoc { id: row.id, yaml, design_json, name, code: row.code, remark: row.remark }) } pub async fn get_by_code(db: &Db, code: &str) -> anyhow::Result { @@ -152,7 +168,12 @@ pub async fn get_by_code(db: &Db, code: &str) -> anyhow::Result { let row = row.ok_or_else(|| anyhow::anyhow!("flow not found with code: {}", code))?; let yaml = row.yaml.unwrap_or_default(); let design_json = row.design_json.and_then(|s| serde_json::from_str::(&s).ok()); - Ok(FlowDoc { id: row.id, yaml, design_json }) + // 名称兜底:数据库 name 为空时,尝试从 YAML 提取 + let name = row + .name + .clone() + .or_else(|| extract_name(&yaml)); + Ok(FlowDoc { id: row.id, yaml, design_json, name, code: row.code, remark: row.remark }) } pub async fn update(db: &Db, id: &str, req: FlowUpdateReq) -> anyhow::Result { @@ -185,7 +206,7 @@ pub async fn update(db: &Db, id: &str, req: FlowUpdateReq) -> anyhow::Result(&s).ok()); - Ok(FlowDoc { id: id.to_string(), yaml: got.yaml.unwrap_or_default(), design_json: dj }) + Ok(FlowDoc { id: id.to_string(), yaml: got.yaml.unwrap_or_default(), design_json: dj, name: got.name, code: got.code, remark: got.remark }) } pub async fn delete(db: &Db, id: &str) -> anyhow::Result<()> { diff --git a/frontend/src/flows/components/tools/index.tsx b/frontend/src/flows/components/tools/index.tsx index 6ae09fb..d075433 100644 --- a/frontend/src/flows/components/tools/index.tsx +++ b/frontend/src/flows/components/tools/index.tsx @@ -14,7 +14,7 @@ import { TestRunButton } from '../testrun/testrun-button'; import { AddNode } from '../add-node'; import { ZoomSelect } from './zoom-select'; import { SwitchLine } from './switch-line'; -import { ToolContainer, ToolSection } from './styles'; +import { ToolContainer, ToolSection, FlowInfoBadge } from './styles'; import { Readonly } from './readonly'; import { MinimapSwitch } from './minimap-switch'; import { Minimap } from './minimap'; @@ -29,6 +29,8 @@ import { Save } from './save'; // 基础信息编辑:对齐新建流程弹窗,使用 Antd 的 Modal/Form import { Modal as AModal, Form as AForm, Input as AInput, message } from 'antd' import api from '../../../utils/axios' +// 新增:兜底从 YAML 提取流程名称 +import { parseFlowYaml } from '../../utils/yaml' // 兼容 BrowserRouter 与 HashRouter:优先从 search 获取,若无则从 hash 的查询串中获取 function getFlowIdFromUrl(): string { @@ -70,6 +72,44 @@ export const FlowTools = () => { const [baseLoading, setBaseLoading] = useState(false) const [baseForm] = AForm.useForm() + // 新增:显示名称与编码的状态 + const [metaLoading, setMetaLoading] = useState(false) + const [flowName, setFlowName] = useState('') + const [flowCode, setFlowCode] = useState('') + + const loadFlowMeta = async () => { + const id = getFlowIdFromUrl() + if (!id) { setFlowName(''); setFlowCode(''); return } + try{ + setMetaLoading(true) + const { data } = await api.get(`/flows/${id}`) + const detail: any = data?.data || {} + const yaml = String(detail?.yaml || '') + const nameFromYaml = (() => { try { return parseFlowYaml(yaml).name || '' } catch { return '' } })() + const nextName = String(detail?.name || nameFromYaml || '') + const nextCode = String(detail?.code || id) + setFlowName(nextName) + setFlowCode(nextCode) + }catch(e:any){ + // 失败时至少展示 ID,确保可识别 + setFlowName('') + setFlowCode(id) + }finally{ + setMetaLoading(false) + } + } + + useEffect(() => { + loadFlowMeta() + const handler = () => loadFlowMeta() + window.addEventListener('hashchange', handler) + window.addEventListener('popstate', handler) + return () => { + window.removeEventListener('hashchange', handler) + window.removeEventListener('popstate', handler) + } + }, []) + const openBaseInfo = async () => { setBaseOpen(true) // 预填现有数据 @@ -111,6 +151,8 @@ export const FlowTools = () => { if (data?.code === 0){ message.success('已保存') setBaseOpen(false) + // 保存后刷新顶部信息 + loadFlowMeta() }else{ throw new Error(data?.message || '保存失败') } @@ -124,6 +166,19 @@ export const FlowTools = () => { return ( <> + {/* 左上角流程信息展示 */} + {(flowName || flowCode) && ( + + {flowCode ? {flowCode} : null} + {flowName ? {flowName} : null} + + + } onClick={openBaseInfo} /> + + + + )} + {/* 返回列表 */} @@ -136,16 +191,6 @@ export const FlowTools = () => { /> - {/* 基础信息(名称/编号/备注) */} - - } - onClick={openBaseInfo} - /> - - diff --git a/frontend/src/flows/components/tools/styles.tsx b/frontend/src/flows/components/tools/styles.tsx index ec1a0ff..0c42881 100644 --- a/frontend/src/flows/components/tools/styles.tsx +++ b/frontend/src/flows/components/tools/styles.tsx @@ -50,3 +50,50 @@ export const MinimapContainer = styled.div` export const UIIconMinimap = styled(IconMinimap)<{ visible: boolean }>` color: ${(props) => (props.visible ? undefined : '#060709cc')}; `; + +// 新增:左上角流程信息展示(编码 + 名称) +export const FlowInfoBadge = styled.div` + position: absolute; + top: 16px; + left: 50%; + transform: translateX(-50%); + display: inline-flex; + align-items: center; + gap: 8px; + background-color: #fff; + border: 1px solid rgba(68, 83, 130, 0.25); + border-radius: 8px; + box-shadow: rgba(0, 0, 0, 0.04) 0px 2px 6px 0px, rgba(0, 0, 0, 0.02) 0px 4px 12px 0px; + height: 40px; + padding: 0 10px; + z-index: 100; + pointer-events: auto; + max-width: 60vw; + + .code { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 12px; + color: #2f54eb; + background: #f0f5ff; + border: 1px solid #adc6ff; + border-radius: 6px; + padding: 2px 6px; + white-space: nowrap; + } + + .name { + font-size: 14px; + font-weight: 600; + color: #1f2937; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .actions { + margin-left: 8px; + display: inline-flex; + align-items: center; + gap: 4px; + } +`;