完善条件判断的删除添加逻辑,添加编排卡片折叠功能

This commit is contained in:
fofolee
2025-01-01 23:07:47 +08:00
parent f3a01a1ba9
commit 37ebebc5ac
7 changed files with 727 additions and 358 deletions

View File

@@ -1,9 +1,83 @@
<template>
<component :is="'style'" v-if="chainStyles">{{ chainStyles }}</component>
<component :is="'style'" v-if="styles">{{ styles }}</component>
</template>
<script>
import { defineComponent } from "vue";
import { defineComponent, ref, watchEffect, computed } from "vue";
// 样式常量
const STYLE_CONSTANTS = {
goldenRatio: 0.618033988749895,
hueStep: 360 * 0.618033988749895,
indent: 5,
lightSl: "60%, 60%",
darkSl: "60%, 40%",
};
// 工具函数
const generateStyleString = (selector, rules) => {
return `${selector} {
${Object.entries(rules)
.map(
([prop, value]) =>
` ${prop.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${value} !important;`
)
.join("\n")}
}`;
};
const generateShadows = (
parentChainIds,
hue,
indent,
lightSl,
darkSl,
uniqueChainIds
) => {
return parentChainIds.reduce(
(acc, parentChainId, i) => {
const parentIndex = uniqueChainIds.indexOf(parentChainId);
const parentHue = (parentIndex * STYLE_CONSTANTS.hueStep) % 360;
const start = -((i + 2) * indent);
acc.light.push(`${start}px 0 0 0 hsl(${parentHue}, ${lightSl})`);
acc.dark.push(`${start}px 0 0 0 hsl(${parentHue}, ${darkSl})`);
return acc;
},
{
light: [`-${indent}px 0 0 0 hsl(${hue}, ${lightSl})`],
dark: [`-${indent}px 0 0 0 hsl(${hue}, ${darkSl})`],
}
);
};
// 获取链组
const getChainGroups = (commands) => {
const groups = [];
const activeGroups = new Map();
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
if (!cmd.chainId) continue;
if (cmd.commandType === cmd.commandChain?.[0]) {
activeGroups.set(cmd.chainId, {
chainId: cmd.chainId,
startIndex: i,
});
} else if (
cmd.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
) {
const group = activeGroups.get(cmd.chainId);
if (group) {
group.endIndex = i;
groups.push({ ...group });
activeGroups.delete(cmd.chainId);
}
}
}
return groups;
};
export default defineComponent({
name: "ChainStyles",
@@ -13,204 +87,119 @@ export default defineComponent({
required: true,
},
},
computed: {
uniqueChainIds() {
return this.commands
.filter(
(cmd) => cmd.chainId && cmd.commandType === cmd.commandChain?.[0]
)
.map((cmd) => cmd.chainId)
.filter((chainId, index, self) => self.indexOf(chainId) === index);
},
chainGroups() {
const groups = [];
const activeGroups = new Map(); // 用于跟踪活动的组
setup(props) {
const styles = ref("");
const chainGroups = computed(() => getChainGroups(props.commands));
this.commands.forEach((cmd, index) => {
if (cmd.chainId) {
if (cmd.commandType === cmd.commandChain?.[0]) {
// 开始一个新的组
activeGroups.set(cmd.chainId, {
chainId: cmd.chainId,
startIndex: index,
});
} else if (
cmd.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
) {
// 结束一个组
const group = activeGroups.get(cmd.chainId);
if (group) {
group.endIndex = index;
groups.push({ ...group });
activeGroups.delete(cmd.chainId);
// 使用 watchEffect 监听 commands 变化并重新计算样式
watchEffect(() => {
// 如果 commands 为空,不生成样式
if (!props.commands?.length) {
styles.value = "";
return;
}
try {
// 1. 获取唯一的链ID
const uniqueChainIds = [
...new Set(
props.commands
.filter(
(cmd) =>
cmd.chainId && cmd.commandType === cmd.commandChain?.[0]
)
.map((cmd) => cmd.chainId)
),
];
// 2. 使用计算好的链组
const groups = chainGroups.value;
// 3. 生成样式
const styleRules = {};
const { hueStep, indent, lightSl, darkSl } = STYLE_CONSTANTS;
uniqueChainIds.forEach((chainId, index) => {
const hue = (index * hueStep) % 360;
const className = "chain-group-" + chainId;
// 计算深度
const currentGroup = groups.find((g) => g.chainId === chainId);
if (!currentGroup) return;
let depth = 1;
const parents = [];
for (const group of groups) {
if (
group.startIndex < currentGroup.startIndex &&
group.endIndex > currentGroup.endIndex
) {
depth++;
parents.push(group.chainId);
}
}
const shadows = generateShadows(
parents,
hue,
indent,
lightSl,
darkSl,
uniqueChainIds
);
const commonStyle = {
marginLeft: `calc(${indent}px * ${depth - 1})`,
};
styleRules["." + className] = {
...commonStyle,
boxShadow: shadows.light.join(", "),
};
styleRules[".body--dark ." + className] = {
...commonStyle,
boxShadow: shadows.dark.join(", "),
};
});
// 4. 生成最终的样式字符串
styles.value = Object.entries(styleRules)
.map(([selector, rules]) => generateStyleString(selector, rules))
.join("\n\n");
} catch (error) {
console.error("Error generating chain styles:", error);
styles.value = "";
}
});
return {
styles,
getChainGroupClass(index) {
const classes = {};
// 使用计算好的链组
for (const group of chainGroups.value) {
if (index >= group.startIndex && index <= group.endIndex) {
classes[`chain-group-${group.chainId}`] = true;
if (index === group.startIndex) {
classes["chain-start"] = true;
} else if (index === group.endIndex) {
classes["chain-end"] = true;
} else {
classes["chain-middle"] = true;
//清除嵌套控制流程的冲突样式
classes["chain-start"] = false;
classes["chain-end"] = false;
}
}
}
});
return groups;
},
styleConstants() {
return {
goldenRatio: 0.618033988749895,
hueStep: 360 * 0.618033988749895,
indent: 5,
lightSl: "60%, 60%",
darkSl: "60%, 40%",
};
},
chainStylesMap() {
const styles = {};
const { hueStep, indent, lightSl, darkSl } = this.styleConstants;
// 先添加圆角样式规则
styles[".chain-start"] = { borderRadius: "4px 4px 0 0" };
styles[".chain-end"] = { borderRadius: "0 4px 0 4px" };
styles[".chain-middle"] = { borderRadius: "0 4px 0 0" };
this.uniqueChainIds.forEach((chainId, index) => {
const hue = (index * hueStep) % 360;
const className = "chain-group-" + chainId;
const depth = this.getChainDepth(chainId);
const parentChainIds = this.getParentChainIds(chainId);
// 生成阴影
const shadows = parentChainIds.reduce(
(acc, parentChainId, i) => {
const parentIndex = this.uniqueChainIds.indexOf(parentChainId);
const parentHue = (parentIndex * hueStep) % 360;
const start = -((i + 2) * indent);
acc.light.push(
start + "px 0 0 0 hsl(" + parentHue + ", " + lightSl + ")"
);
acc.dark.push(
start + "px 0 0 0 hsl(" + parentHue + ", " + darkSl + ")"
);
return acc;
},
{
light: [-indent + "px 0 0 0 hsl(" + hue + ", " + lightSl + ")"],
dark: [-indent + "px 0 0 0 hsl(" + hue + ", " + darkSl + ")"],
}
);
// 基础样式
const commonStyle = {
marginLeft: "calc(" + indent + "px * " + (depth - 1) + ")",
};
// 生成样式规则
styles["." + className] = {
...commonStyle,
background: "hsla(" + hue + ", " + lightSl + ", 0.15)",
boxShadow: shadows.light.join(", "),
};
// 暗色模式样式
styles[".body--dark ." + className] = {
...commonStyle,
background: "hsla(" + hue + ", " + darkSl + ", 0.2)",
boxShadow: shadows.dark.join(", "),
};
});
return styles;
},
chainStyles() {
return Object.entries(this.chainStylesMap)
.map(
([selector, rules]) =>
selector +
" {\n" +
Object.entries(rules)
.map(
([prop, value]) =>
" " +
prop.replace(/([A-Z])/g, "-$1").toLowerCase() +
": " +
value +
" !important;"
)
.join("\n") +
"\n}"
)
.join("\n\n");
},
},
methods: {
getChainDepth(chainId) {
let depth = 1;
let currentIndex = this.commands.findIndex(
(cmd) =>
cmd.chainId === chainId && cmd.commandType === cmd.commandChain?.[0]
);
if (currentIndex === -1) return depth;
for (let i = 0; i < currentIndex; i++) {
const cmd = this.commands[i];
if (cmd.chainId && cmd.commandType === cmd.commandChain?.[0]) {
const endIndex = this.commands.findIndex(
(c, idx) =>
idx > i &&
c.chainId === cmd.chainId &&
c.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
);
if (endIndex > currentIndex) {
depth++;
}
}
}
return depth;
},
getParentChainIds(chainId) {
const parents = [];
let currentIndex = this.commands.findIndex(
(cmd) =>
cmd.chainId === chainId && cmd.commandType === cmd.commandChain?.[0]
);
if (currentIndex === -1) return parents;
for (let i = currentIndex - 1; i >= 0; i--) {
const cmd = this.commands[i];
if (cmd.chainId && cmd.commandType === cmd.commandChain?.[0]) {
const endIndex = this.commands.findIndex(
(c, idx) =>
idx > i &&
c.chainId === cmd.chainId &&
c.commandType === cmd.commandChain?.[cmd.commandChain.length - 1]
);
if (endIndex > currentIndex) {
parents.push(cmd.chainId);
}
}
}
return parents;
},
getChainGroupClass(index) {
// 找出所有包含当前索引的组
const matchingGroups = this.chainGroups.filter(
(g) => index >= g.startIndex && index <= g.endIndex
);
// 返回所有匹配组的类名
const classes = {};
matchingGroups.forEach((group) => {
classes["chain-group-" + group.chainId] = true;
if (index === group.startIndex) {
classes["chain-start"] = true;
}
if (index === group.endIndex) {
classes["chain-end"] = true;
}
if (index > group.startIndex && index < group.endIndex) {
classes["chain-middle"] = true;
}
});
return classes;
},
return classes;
},
};
},
});
</script>

View File

@@ -0,0 +1,208 @@
<template>
<div class="composer-buttons">
<div class="left-buttons">
<q-btn
:icon="isAllCollapsed ? 'unfold_more' : 'unfold_less'"
dense
flat
@click="$emit('action', isAllCollapsed ? 'expandAll' : 'collapseAll')"
>
<q-tooltip>{{ isAllCollapsed ? "展开所有" : "折叠所有" }}</q-tooltip>
</q-btn>
</div>
<div class="right-buttons">
<q-btn
@click="$q.dark.toggle()"
:icon="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
flat
dense
v-if="isDev"
>
</q-btn>
<q-btn icon="logout" dense flat v-close-popup>
<q-tooltip>退出可视化编排</q-tooltip>
</q-btn>
<q-btn dense icon="publish" flat @click="$emit('action', 'insert')">
<q-tooltip>插入到编辑器光标处</q-tooltip>
</q-btn>
<q-btn dense flat icon="done_all" @click="$emit('action', 'apply')">
<q-tooltip>清空编辑器内容并插入</q-tooltip>
</q-btn>
<q-btn
flat
dense
icon="preview"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
</q-btn>
<q-btn dense flat icon="play_circle" @click="$emit('action', 'run')">
<q-tooltip>运行</q-tooltip>
</q-btn>
</div>
<transition name="preview-fade">
<div v-if="isVisible" class="preview-popup">
<div class="preview-header">
<q-icon name="code" size="16px" class="q-mr-xs" />
<span>预览代码</span>
</div>
<pre class="preview-code"><code>{{ code }}</code></pre>
</div>
</transition>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "ComposerButtons",
props: {
generateCode: {
type: Function,
required: true,
},
isAllCollapsed: {
type: Boolean,
default: false,
},
},
emits: ["action"],
data() {
return {
isVisible: false,
code: "",
previewTimer: null,
isDev: window.utools.isDev(),
};
},
methods: {
handleMouseEnter() {
this.previewTimer = setTimeout(() => {
this.code = this.generateCode();
this.isVisible = true;
}, 200);
},
handleMouseLeave() {
clearTimeout(this.previewTimer);
this.isVisible = false;
},
},
beforeUnmount() {
clearTimeout(this.previewTimer);
},
});
</script>
<style scoped>
.composer-buttons {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.left-buttons,
.right-buttons {
display: flex;
align-items: center;
}
.composer-buttons > div > .q-btn {
opacity: 0.6;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 12px;
}
.composer-buttons > div > .q-btn:hover {
opacity: 1;
transform: translateY(-1px);
color: var(--q-primary);
}
.preview-popup {
position: absolute;
top: 40px;
right: 30px;
min-width: 300px;
max-width: 600px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
transform-origin: center right;
}
.preview-header {
padding: 10px 14px;
background: rgba(var(--q-primary-rgb), 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 8px 8px 0 0;
font-size: 13px;
font-weight: 500;
color: var(--q-primary);
display: flex;
align-items: center;
}
.preview-code {
margin: 0;
padding: 14px;
font-family: consolas, monaco, monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
/* 自定义滚动条 */
.preview-code::-webkit-scrollbar {
width: 6px;
}
.preview-code::-webkit-scrollbar-thumb {
background: var(--q-primary-opacity-20);
border-radius: 3px;
transition: background 0.3s;
}
.preview-code::-webkit-scrollbar-thumb:hover {
background: var(--q-primary-opacity-30);
}
/* 过渡动画 */
.preview-fade-enter-active,
.preview-fade-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-fade-enter-from,
.preview-fade-leave-to {
opacity: 0;
transform: translateX(20px) scale(0.95);
}
/* 暗色模式适配 */
.body--dark .preview-popup {
background: #1d1d1d;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.body--dark .preview-header {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .preview-code {
color: #e0e0e0;
}
</style>