1. 上游节点输出引用(核心功能)
- 新增 upstreamNodes computed:遍历当前选中节点的前驱链路,仅 model / http / form 三类节点被识别为有输出的节点
- 新增 getNodeOutputFields,按节点类型提取可引用字段:
- form → formConfig 的字段
- http → response schema 的叶子字段
- model → modelResponseBodyMapping(模型返回参数)的 key
- 开始节点 → runFormFields(运行表单字段),从而让下游节点能引用"用户填的运行表单"作为输出
2. 开始节点运行表单字段聚合
- syncRunFormFields + 深监听:开始节点 formConfig 变化时自动聚合字段,存为 runFormFields,作为开始节点自身可被下游引用的输出
3. 模型返回参数
- handleModelConfirm 用 stripReadonlyFields 清理 requestBodyMapping 冗余字段,同时保存 modelResponseBodyMapping 供下游引用
- 保存时 collectExposedFields 提取对外暴露字段存 modelFormFields
4. 提示词 / 反向提示词
- 保存 prompt、negativePrompt、runFormFields 到节点 DSL
5. 其他
- nodeTypes 用 markRaw 包裹 FlowNode,修复 Vue 警告
- buildNodeFormConfigFromDsl 的 responseType 匹配改为兼容 key/value 两种取值
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<div class="model-field">
|
||||
<!-- object:递归渲染 attrs 子字段 -->
|
||||
<template v-if="isObject">
|
||||
<div class="mf-block">
|
||||
<div class="mf-block-title">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<span class="mf-block-name">{{ label }}</span> <el-tooltip v-if="description" :content="description">
|
||||
<el-icon class="mf-info"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="mf-block-body">
|
||||
<ModelField
|
||||
v-for="(childDef, childKey) in objectChildren"
|
||||
:key="childKey"
|
||||
:field-def="childDef"
|
||||
:path="path ? `${path}.attrs.${childKey}` : childKey"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- array:直接平铺展示元素子字段,用户直接填写;不提供添加/删除 -->
|
||||
<template v-else-if="isArray">
|
||||
<div class="mf-block">
|
||||
<div class="mf-block-title">
|
||||
<el-icon><List /></el-icon>
|
||||
<span class="mf-block-name">{{ label }}</span>
|
||||
<span v-if="required" class="mf-required">*</span> <el-tooltip v-if="description" :content="description">
|
||||
<el-icon class="mf-info"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="mf-array-flat">
|
||||
<!-- 联合数组(enumValues 多模板):全部模板平铺,固定条数,不可增删 -->
|
||||
<template v-if="isVariantArray">
|
||||
<div v-for="(item, idx) in getVariantItems()" :key="idx" class="mf-variant">
|
||||
<div class="mf-variant-head">
|
||||
<span class="mf-variant-name">{{ variantLabel(idx) }}</span>
|
||||
</div>
|
||||
<template v-if="isObjectDef(item)">
|
||||
<ModelField
|
||||
v-for="(subDef, subKey) in item.attrs"
|
||||
:key="subKey"
|
||||
:field-def="subDef"
|
||||
:path="path ? `${path}.value[${idx}].attrs.${subKey}` : String(subKey)"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-else :model-value="item" @input="(v: any) => setArrayPrimitive(idx, v)" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 同构数组:保持现有平铺逻辑 -->
|
||||
<template v-else>
|
||||
<template v-for="(item, idx) in getArrayValue()" :key="idx">
|
||||
<template v-if="isObjectDef(item)">
|
||||
<div v-if="getArrayValue().length > 1" class="mf-array-flat-index">#{{ idx + 1 }}</div>
|
||||
<ModelField
|
||||
v-for="(subDef, subKey) in item.attrs"
|
||||
:key="subKey"
|
||||
:field-def="subDef"
|
||||
:path="path ? `${path}.value[${idx}].attrs.${subKey}` : String(subKey)"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-else :model-value="item" @input="(v: any) => setArrayPrimitive(idx, v)" size="small" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 叶子:按 fieldType 渲染控件 -->
|
||||
<template v-else>
|
||||
<div class="mf-leaf">
|
||||
<div class="mf-leaf-label">
|
||||
<span class="mf-label-text">{{ label }}</span>
|
||||
<span v-if="required" class="mf-required">*</span> <el-tooltip v-if="description" :content="description">
|
||||
<el-icon class="mf-info"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="path || label" placement="top">
|
||||
<el-checkbox
|
||||
size="small"
|
||||
:model-value="def.runtimeShow ?? false"
|
||||
:disabled="hasReference"
|
||||
@change="toggleExpose"
|
||||
class="mf-leaf-expose"
|
||||
>表单展示</el-checkbox>
|
||||
</el-tooltip>
|
||||
<el-popover
|
||||
v-if="canReference"
|
||||
placement="bottom"
|
||||
:width="260"
|
||||
trigger="click"
|
||||
popper-class="mf-ref-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
class="mf-ref-btn"
|
||||
title="引用上级节点输出"
|
||||
:disabled="!!def.runtimeShow || !hasUpstream"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="mf-ref-panel">
|
||||
<div v-if="!hasUpstream" class="mf-ref-empty">无可引用的上级节点</div>
|
||||
<template v-else>
|
||||
<div v-for="node in upstreamNodes" :key="node.id" class="mf-ref-node">
|
||||
<div class="mf-ref-node-head">
|
||||
<span class="mf-ref-node-name">{{ node.label }}</span>
|
||||
<el-tag size="small" type="info">{{ node.nodeCode }}</el-tag>
|
||||
</div>
|
||||
<div v-if="node.outputFields.length === 0" class="mf-ref-node-empty">该节点无输出字段</div>
|
||||
<div
|
||||
v-for="of in node.outputFields"
|
||||
:key="of.field"
|
||||
class="mf-ref-field"
|
||||
@click="setReference(node, of)"
|
||||
>
|
||||
{{ of.label || of.field }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div v-if="hasReference" class="mf-ref-bound" :class="{ 'is-invalid': referenceInvalid }">
|
||||
<span class="mf-ref-bound-text">已引用:{{ referenceLabel }}</span>
|
||||
<el-button size="small" text type="danger" @click="clearReference">清除</el-button>
|
||||
</div>
|
||||
<div v-else-if="!isUpload" class="mf-leaf-ctrl">
|
||||
<el-input-number
|
||||
v-if="isNumber"
|
||||
:model-value="numberValue"
|
||||
@change="setValue"
|
||||
class="mf-ctrl"
|
||||
:controls="false"
|
||||
:placeholder="required ? '必填' : ''"
|
||||
/>
|
||||
<el-switch v-else-if="isSwitch" :model-value="switchValue" @change="setValue" class="mf-ctrl" />
|
||||
<el-select
|
||||
v-else-if="isSelect"
|
||||
:model-value="def.value ?? ''"
|
||||
@change="setSelectValue"
|
||||
class="mf-ctrl"
|
||||
clearable
|
||||
:placeholder="required ? '必填' : ''"
|
||||
>
|
||||
<el-option v-for="opt in def.options" :key="opt.value" :label="opt.label ?? opt.value" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else-if="isTextarea"
|
||||
:model-value="def.value ?? ''"
|
||||
@input="setValue"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
class="mf-ctrl"
|
||||
:placeholder="required ? '必填' : ''"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="def.value ?? ''"
|
||||
@input="setValue"
|
||||
class="mf-ctrl"
|
||||
:placeholder="required ? '必填' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import { FolderOpened, List, QuestionFilled, Link } from '@element-plus/icons-vue';
|
||||
import { deepClone, normalizeArrayItem } from './modelParamUtils';
|
||||
|
||||
defineOptions({ name: 'ModelField' });
|
||||
|
||||
// 前驱节点的可引用输出字段(供「引用上级节点输出」)
|
||||
interface UpstreamNodeInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
nodeCode: string;
|
||||
outputFields: { field: string; label: string }[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[] }>();
|
||||
|
||||
// 用 computed 实时取当前 props,避免 internalSync 替换对象树后仍持有旧引用
|
||||
const def = computed(() => props.fieldDef);
|
||||
|
||||
// ===== 基础元信息 =====
|
||||
const label = computed(() => def.value?.label || def.value?.field || '');
|
||||
const description = computed(() => def.value?.description || '');
|
||||
const required = computed(() => !!def.value?.required);
|
||||
const defType = computed(() => def.value?.type || 'string');
|
||||
const fieldType = computed(() => def.value?.fieldType || 'string');
|
||||
|
||||
// ===== 类型判定 =====
|
||||
const isObject = computed(() => defType.value === 'object');
|
||||
const isArray = computed(() => defType.value === 'array');
|
||||
const isLeaf = computed(() => !isObject.value && !isArray.value);
|
||||
|
||||
const objectChildren = computed<Record<string, any>>(() => {
|
||||
if (!isObject.value) return {};
|
||||
const attrs = def.value?.attrs;
|
||||
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) return attrs;
|
||||
return {};
|
||||
});
|
||||
|
||||
const isNumber = computed(() => isLeaf.value && (fieldType.value === 'number' || defType.value === 'number'));
|
||||
const isSwitch = computed(() => isLeaf.value && (fieldType.value === 'switch' || defType.value === 'boolean'));
|
||||
const isSelect = computed(() => isLeaf.value && fieldType.value === 'select' && Array.isArray(def.value?.options));
|
||||
const isTextarea = computed(() => isLeaf.value && fieldType.value === 'textarea');
|
||||
const isUpload = computed(() => isLeaf.value && fieldType.value === 'upload');
|
||||
|
||||
// ===== 引用上级节点输出 =====
|
||||
// 可引用:叶子 + 非开关 + 非固定选项下拉 + 非上传(开关/下拉值来自模型;上传类型不提供引用)
|
||||
const canReference = computed(() => isLeaf.value && !isSwitch.value && !isSelect.value && !isUpload.value);
|
||||
const hasUpstream = computed(() => (props.upstreamNodes?.length ?? 0) > 0);
|
||||
const hasReference = computed(() => !!def.value?.reference);
|
||||
// 已引用时的展示文案:{节点label}.{字段label}
|
||||
const referenceLabel = computed(() => {
|
||||
const ref = def.value?.reference;
|
||||
if (!ref) return '';
|
||||
const node = props.upstreamNodes?.find((n) => n.id === ref.nodeId);
|
||||
const nodeName = node?.label || ref.nodeId;
|
||||
const fieldLabel = node?.outputFields.find((f) => f.field === ref.field)?.label || ref.field;
|
||||
return `${nodeName}.${fieldLabel}`;
|
||||
});
|
||||
|
||||
// 引用是否已失效:上游节点被删除,或其输出字段已不存在(换模型/换配置导致 schema 变化)
|
||||
const referenceInvalid = computed(() => {
|
||||
const ref = def.value?.reference;
|
||||
if (!ref) return false;
|
||||
const node = props.upstreamNodes?.find((n) => n.id === ref.nodeId);
|
||||
if (!node) return true;
|
||||
return !node.outputFields.some((f) => f.field === ref.field);
|
||||
});
|
||||
|
||||
// 失效引用自动清理,避免保存脏数据(就地删除 reference,复用深 watch 上传链路)
|
||||
watch(
|
||||
referenceInvalid,
|
||||
(invalid) => {
|
||||
if (invalid && def.value) {
|
||||
delete def.value.reference;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 选中前驱节点输出字段:就地写 def.reference,复用深 watch 上传链路
|
||||
const setReference = (node: UpstreamNodeInfo, of: { field: string; label: string }) => {
|
||||
if (!def.value) return;
|
||||
def.value.reference = { nodeId: node.id, field: of.field };
|
||||
};
|
||||
|
||||
const clearReference = () => {
|
||||
if (!def.value) return;
|
||||
delete def.value.reference;
|
||||
};
|
||||
|
||||
// ===== 叶子值(兼容字符串/布尔/数字) =====
|
||||
const numberValue = computed(() => {
|
||||
const v = def.value?.value !== undefined ? def.value.value : def.value?.defaultValue;
|
||||
const n = Number(v);
|
||||
return Number.isNaN(n) ? 0 : n;
|
||||
});
|
||||
const switchValue = computed(() => {
|
||||
const v = def.value?.value !== undefined ? def.value.value : def.value?.defaultValue;
|
||||
return v === true || v === 'true';
|
||||
});
|
||||
|
||||
const setValue = (val: any) => {
|
||||
if (!def.value) return;
|
||||
if (isSwitch.value) {
|
||||
def.value.value = val === true || val === 'true';
|
||||
} else if (isNumber.value) {
|
||||
def.value.value = val === null || val === undefined || val === '' ? 0 : Number(val);
|
||||
} else {
|
||||
def.value.value = val ?? '';
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectValue = (val: any) => {
|
||||
if (!def.value) return;
|
||||
def.value.value = val ?? '';
|
||||
};
|
||||
|
||||
|
||||
// 勾选「表单展示」:就地写 def.runtimeShow,复用深 watch 上传链路
|
||||
const toggleExpose = (checked: any) => {
|
||||
if (!def.value) return;
|
||||
def.value.runtimeShow = !!checked;
|
||||
};
|
||||
|
||||
|
||||
// ===== array:平铺展示元素子字段,供用户直接填写;不提供添加/删除 =====
|
||||
// 元素模板:优先 enumValues 首个,其次 attrs 数组,其次单个 attrs 对象
|
||||
const arrayTemplates = computed<any[]>(() => {
|
||||
if (!isArray.value || !def.value) return [];
|
||||
if (Array.isArray(def.value.enumValues) && def.value.enumValues.length) return def.value.enumValues;
|
||||
if (Array.isArray(def.value.attrs) && def.value.attrs.length) return def.value.attrs;
|
||||
if (def.value.attrs && typeof def.value.attrs === 'object' && !Array.isArray(def.value.attrs)) {
|
||||
return [{ type: 'object', attrs: def.value.attrs }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const getArrayValue = (): any[] => {
|
||||
if (!def.value) return [];
|
||||
if (!Array.isArray(def.value.value)) {
|
||||
def.value.value = [];
|
||||
}
|
||||
// value 为空时(后端仅定义结构未给内容)用模板补一条空结构,保证字段可展示、可填写
|
||||
if (def.value.value.length === 0 && arrayTemplates.value.length > 0) {
|
||||
def.value.value.push(normalizeArrayItem(deepClone(arrayTemplates.value[0])));
|
||||
}
|
||||
return def.value.value;
|
||||
};
|
||||
|
||||
const isObjectDef = (item: any) => !!item && typeof item === 'object' && item.type === 'object' && item.attrs && typeof item.attrs === 'object';
|
||||
|
||||
const setArrayPrimitive = (idx: number, val: any) => {
|
||||
getArrayValue()[idx] = val ?? '';
|
||||
};
|
||||
|
||||
// ===== array:联合数组(enumValues 多模板)→ 全部模板平铺,固定条数,不可增删 =====
|
||||
const isVariantArray = computed(
|
||||
() => isArray.value && Array.isArray(def.value?.enumValues) && (def.value?.enumValues?.length ?? 0) > 1
|
||||
);
|
||||
|
||||
// 变体名推断:优先 type 字段值(如 text/image_url/video_url),其次 role 字段描述里的角色名,兜底"变体 N"
|
||||
const templateLabel = (tpl: any, index: number): string => {
|
||||
if (tpl && typeof tpl === 'object' && tpl.attrs && typeof tpl.attrs === 'object' && !Array.isArray(tpl.attrs)) {
|
||||
const typeField = tpl.attrs.type;
|
||||
if (typeField) {
|
||||
const t = typeField.value ?? typeField.defaultValue ?? '';
|
||||
if (t) return String(t);
|
||||
}
|
||||
const roleField = tpl.attrs.role;
|
||||
if (roleField && roleField.description) {
|
||||
const m = String(roleField.description).match(/(此处为|固定为|为)\s*([a-zA-Z0-9_-]+)/);
|
||||
if (m) return m[1];
|
||||
}
|
||||
}
|
||||
return `变体 ${index + 1}`;
|
||||
};
|
||||
|
||||
// 自动填标识值:type 用 defaultValue,role 从 description 提取;只填空值,不覆盖用户已填
|
||||
const autoFillVariant = (tpl: any): void => {
|
||||
if (!tpl || typeof tpl !== 'object' || !tpl.attrs || typeof tpl.attrs !== 'object' || Array.isArray(tpl.attrs)) return;
|
||||
const typeField = tpl.attrs.type;
|
||||
if (typeField && (typeField.value === undefined || typeField.value === null || typeField.value === '')) {
|
||||
if (typeField.defaultValue !== undefined && typeField.defaultValue !== null) typeField.value = typeField.defaultValue;
|
||||
}
|
||||
const roleField = tpl.attrs.role;
|
||||
if (roleField && (roleField.value === undefined || roleField.value === null || roleField.value === '')) {
|
||||
const m = String(roleField.description || '').match(/(此处为|固定为|为)\s*([a-zA-Z0-9_-]+)/);
|
||||
if (m) roleField.value = m[1];
|
||||
}
|
||||
};
|
||||
|
||||
// 联合数组渲染条目:value 与 enumValues 模板按索引一一对应(懒补模板),保证固定条数
|
||||
const getVariantItems = (): any[] => {
|
||||
if (!isVariantArray.value || !def.value) return [];
|
||||
const templates = Array.isArray(def.value.enumValues) ? def.value.enumValues : [];
|
||||
if (!Array.isArray(def.value.value)) def.value.value = [];
|
||||
templates.forEach((tpl: any, i: number) => {
|
||||
const existing = def.value.value[i];
|
||||
if (!existing || typeof existing !== 'object') {
|
||||
const item = normalizeArrayItem(deepClone(tpl));
|
||||
autoFillVariant(item);
|
||||
def.value.value[i] = item;
|
||||
}
|
||||
});
|
||||
if (def.value.value.length > templates.length) {
|
||||
def.value.value = def.value.value.slice(0, templates.length);
|
||||
}
|
||||
return def.value.value;
|
||||
};
|
||||
|
||||
const variantLabel = (idx: number): string => templateLabel(def.value?.enumValues?.[idx], idx);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-field {
|
||||
width: 100%;
|
||||
|
||||
// 嵌套层级不重复套卡片:内层块去边框背景,用左缩进区分层级
|
||||
.model-field {
|
||||
.mf-block,
|
||||
.mf-leaf {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0 0 0 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mf-block {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
background: #fbfcfe;
|
||||
|
||||
.mf-block-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mf-block-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-block-body {
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-info {
|
||||
cursor: help;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mf-required {
|
||||
color: #f56c6c;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mf-array-flat {
|
||||
.mf-array-flat-index {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
background: #eef2f7;
|
||||
border-radius: 999px;
|
||||
padding: 0 6px;
|
||||
margin: 2px 0 6px;
|
||||
}
|
||||
|
||||
:deep(.model-field) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-variant {
|
||||
margin-bottom: 8px;
|
||||
padding-left: 10px;
|
||||
border-left: 2px solid #e2e8f0;
|
||||
|
||||
.mf-variant-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.mf-variant-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mf-leaf {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 10px;
|
||||
background: #ffffff;
|
||||
|
||||
.mf-leaf-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
|
||||
.mf-label-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mf-leaf-expose {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-ref-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-ref-bound {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
|
||||
.mf-ref-bound-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-leaf-ctrl {
|
||||
.mf-ctrl {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- popover teleport 到 body,面板样式需脱离 scoped -->
|
||||
<style lang="scss">
|
||||
.mf-ref-popover {
|
||||
.mf-ref-panel {
|
||||
.mf-ref-empty {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mf-ref-node {
|
||||
& + .mf-ref-node {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.mf-ref-node-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.mf-ref-node-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-ref-node-empty {
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
padding: 2px 0 2px 8px;
|
||||
}
|
||||
|
||||
.mf-ref-field {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user