优化可编排界面的样式

This commit is contained in:
fofolee 2024-12-22 22:08:02 +08:00
parent 2c8db1f473
commit 29ceb3c7ff
5 changed files with 464 additions and 161 deletions

View File

@ -169,16 +169,12 @@
<KeyRecorder @sendKeys="addAction" /> <KeyRecorder @sendKeys="addAction" />
</q-dialog> </q-dialog>
<q-dialog v-model="showComposer" maximized> <q-dialog v-model="showComposer" maximized>
<q-card class="full-height"> <CommandComposer
<q-card-section class="q-pa-md full-height"> ref="composer"
<CommandComposer @run="handleComposerRun"
ref="composer" @apply="handleComposerApply"
@run="handleComposerRun" @update:model-value="showComposer = false"
@apply="handleComposerApply" />
@update:model-value="showComposer = false"
/>
</q-card-section>
</q-card>
</q-dialog> </q-dialog>
</div> </div>
</template> </template>
@ -298,19 +294,19 @@ export default {
window.showUb.docs(); window.showUb.docs();
}, },
handleComposerRun(code) { handleComposerRun(code) {
this.$emit('add-action', code); this.$emit("add-action", code);
}, },
handleComposerApply(code) { handleComposerApply(code) {
this.$emit('add-action', code); this.$emit("add-action", code);
this.showComposer = false; this.showComposer = false;
}, },
applyComposerCommands() { applyComposerCommands() {
if (this.$refs.composer) { if (this.$refs.composer) {
const code = this.$refs.composer.generateCode(); const code = this.$refs.composer.generateCode();
this.$emit('add-action', code); this.$emit("add-action", code);
} }
this.showComposer = false; this.showComposer = false;
} },
}, },
}; };
</script> </script>

View File

@ -1,18 +1,11 @@
<template> <template>
<div class="command-composer"> <div class="command-composer">
<!-- 固定头部 -->
<div class="composer-header q-pa-sm row items-center bg-white">
<div class="text-h6 text-weight-medium">可视化命令编排</div>
<q-space />
<q-btn flat round dense icon="close" v-close-popup />
</div>
<!-- 主体内容 --> <!-- 主体内容 -->
<div class="composer-body row no-wrap q-pa-sm"> <div class="composer-body row no-wrap q-pa-sm">
<!-- 左侧命令列表 --> <!-- 左侧命令列表 -->
<div class="col-3"> <div class="col-3 command-section">
<div class="text-subtitle1 q-pb-sm">可用命令</div> <div class="text-subtitle1 q-pb-sm">可用命令</div>
<q-scroll-area style="height: calc(100vh - 200px)"> <q-scroll-area class="command-scroll">
<ComposerList <ComposerList
:commands="availableCommands" :commands="availableCommands"
@add-command="addCommand" @add-command="addCommand"
@ -21,16 +14,16 @@
</div> </div>
<!-- 右侧命令流程 --> <!-- 右侧命令流程 -->
<div class="col q-pl-md"> <div class="col q-pl-md command-section">
<div class="text-subtitle1 q-pb-sm">命令流程</div> <div class="text-subtitle1 q-pb-sm">命令流程</div>
<q-scroll-area style="height: calc(100vh - 200px)"> <q-scroll-area class="command-scroll">
<ComposerFlow v-model="commandFlow" @add-command="addCommand" /> <ComposerFlow v-model="commandFlow" @add-command="addCommand" />
</q-scroll-area> </q-scroll-area>
</div> </div>
</div> </div>
<!-- 固定底部 --> <!-- 固定底部 -->
<div class="composer-footer q-pa-sm row items-center justify-end bg-white"> <div class="composer-footer q-pa-sm row items-center justify-end">
<q-btn <q-btn
outline outline
color="primary" color="primary"
@ -130,22 +123,37 @@ export default defineComponent({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
background-color: #f4f4f4;
} }
.composer-header { .body--dark .command-composer {
border-bottom: 1px solid #e0e0e0; background-color: #303132;
} }
.composer-body { .composer-body {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
background-color: #f5f5f5; }
.command-section {
display: flex;
flex-direction: column;
height: 100%;
}
.command-scroll {
flex: 1;
overflow: hidden;
} }
.composer-footer { .composer-footer {
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
} }
.body--dark .composer-footer {
border-top: 1px solid #676666;
}
/* 滚动美化 */ /* 滚动美化 */
:deep(.q-scrollarea__thumb) { :deep(.q-scrollarea__thumb) {
width: 6px; width: 6px;
@ -156,4 +164,8 @@ export default defineComponent({
:deep(.q-scrollarea__thumb:hover) { :deep(.q-scrollarea__thumb:hover) {
opacity: 0.8; opacity: 0.8;
} }
:deep(.q-scrollarea__content) {
padding-right: 8px;
}
</style> </style>

View File

@ -1,15 +1,18 @@
<template> <template>
<div class="composer-card q-mb-sm"> <div
<q-card flat bordered> class="composer-card q-pa-xs"
<q-card-section horizontal class="q-pa-sm"> :class="{ 'can-drop': canDrop }"
<!-- 拖拽手柄 --> v-bind="$attrs"
<div class="drag-handle cursor-move q-mr-sm"> >
<q-icon name="drag_indicator" size="24px" class="text-grey-6" /> <q-card class="command-item">
</div> <q-card-section class="q-pa-sm">
<div class="col"> <div class="col">
<!-- 命令标题和描述 --> <!-- 命令标题和描述 -->
<div class="row items-center q-mb-sm"> <div class="row items-center q-mb-sm">
<!-- 拖拽手柄 -->
<div class="drag-handle cursor-move q-mr-sm">
<q-icon name="drag_indicator" size="18px" class="text-grey-6" />
</div>
<div class="text-subtitle1">{{ command.label }}</div> <div class="text-subtitle1">{{ command.label }}</div>
<q-space /> <q-space />
<!-- 输出开关 --> <!-- 输出开关 -->
@ -25,6 +28,7 @@
dense dense
icon="close" icon="close"
@click="$emit('remove')" @click="$emit('remove')"
size="sm"
/> />
</div> </div>
@ -49,7 +53,12 @@
</template> </template>
<template v-slot:selected-item="scope"> <template v-slot:selected-item="scope">
<div class="row items-center"> <div class="row items-center">
<q-icon name="output" color="primary" size="xs" class="q-mr-xs" /> <q-icon
name="output"
color="primary"
size="xs"
class="q-mr-xs"
/>
{{ scope.opt.label }} {{ scope.opt.label }}
</div> </div>
</template> </template>
@ -69,7 +78,7 @@
:label="placeholder" :label="placeholder"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="code" /> <q-icon name="text_fields" size="18px" />
</template> </template>
</q-input> </q-input>
</template> </template>
@ -81,87 +90,167 @@
</template> </template>
<script> <script>
import { defineComponent } from 'vue' import { defineComponent } from "vue";
import KeyEditor from './KeyEditor.vue' import KeyEditor from "./KeyEditor.vue";
export default defineComponent({ export default defineComponent({
name: 'ComposerCard', name: "ComposerCard",
components: { components: {
KeyEditor KeyEditor,
}, },
props: { props: {
command: { command: {
type: Object, type: Object,
required: true required: true,
}, },
hasOutput: { hasOutput: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
canUseOutput: { canUseOutput: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
availableOutputs: { availableOutputs: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
placeholder: { placeholder: {
type: String, type: String,
default: '' default: "",
} },
canDrop: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
showKeyRecorder: false showKeyRecorder: false,
} };
}, },
emits: ['remove', 'toggle-output', 'update:argv', 'update:use-output'], emits: ["remove", "toggle-output", "update:argv", "update:use-output"],
computed: { computed: {
saveOutputLocal: { saveOutputLocal: {
get() { get() {
return this.command.saveOutput return this.command.saveOutput;
}, },
set(value) { set(value) {
this.$emit('toggle-output') this.$emit("toggle-output");
} },
}, },
argvLocal: { argvLocal: {
get() { get() {
return this.command.argv return this.command.argv;
}, },
set(value) { set(value) {
this.$emit('update:argv', value) this.$emit("update:argv", value);
} },
}, },
useOutputLocal: { useOutputLocal: {
get() { get() {
return this.command.useOutput return this.command.useOutput;
}, },
set(value) { set(value) {
this.$emit('update:use-output', value) this.$emit("update:use-output", value);
} },
} },
}, },
methods: { methods: {
handleClearOutput() { handleClearOutput() {
this.$emit('update:use-output', null) this.$emit("update:use-output", null);
}, },
handleKeyRecord(keys) { handleKeyRecord(keys) {
this.showKeyRecorder = false; this.showKeyRecorder = false;
// keyTap("a","control") // keyTap("a","control")
const matches = keys.match(/keyTap\((.*)\)/) const matches = keys.match(/keyTap\((.*)\)/);
if (matches && matches[1]) { if (matches && matches[1]) {
this.$emit('update:argv', matches[1]); this.$emit("update:argv", matches[1]);
} }
} },
} },
}) mounted() {
this.$el.classList.add("composer-card-enter-from");
requestAnimationFrame(() => {
this.$el.classList.remove("composer-card-enter-from");
});
},
});
</script> </script>
<style scoped> <style scoped>
.composer-card { .composer-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
opacity: 1;
transform: translateY(0) scale(1);
}
/* 进入动画 */
.composer-card-enter-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.composer-card-enter-from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
/* 移除动画 */
.composer-card-leave-active {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: absolute;
}
.composer-card-leave-to {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
/* 拖拽动画 */
.composer-card:active {
transform: scale(1.02);
transition: transform 0.2s;
}
.command-item {
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.command-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.composer-card :deep(.q-field__label) {
font-size: 13px;
}
/* 可放置状态动画 */
.can-drop {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.can-drop .command-item {
border: 2px dashed var(--q-primary);
background: rgba(var(--q-primary-rgb), 0.05);
}
/* 暗色模式适配 */
.body--dark .command-item {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.body--dark .command-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.body--dark .can-drop {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
.drag-handle { .drag-handle {

View File

@ -2,8 +2,9 @@
<div class="composer-flow"> <div class="composer-flow">
<div <div
class="command-flow-container" class="command-flow-container"
@dragover.prevent @dragover.prevent="onDragOver"
@drop="onDrop" @drop="onDrop"
@dragleave.prevent="onDragLeave"
> >
<draggable <draggable
v-model="commands" v-model="commands"
@ -12,14 +13,19 @@
class="flow-list" class="flow-list"
handle=".drag-handle" handle=".drag-handle"
:animation="200" :animation="200"
@start="onDragStart"
@end="onDragEnd"
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<transition <transition name="slide-fade" mode="out-in" appear>
name="slide-fade" <div
mode="out-in" :key="element.id"
appear class="flow-item"
> :class="{
<div :key="element.id" class="flow-item"> 'insert-before': dragIndex === index,
'insert-after': dragIndex === commands.length && index === commands.length - 1
}"
>
<ComposerCard <ComposerCard
:command="element" :command="element"
:has-output="hasOutput(element)" :has-output="hasOutput(element)"
@ -41,57 +47,132 @@
<div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div> <div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div>
</div> </div>
</div> </div>
<div v-else class="drop-area"></div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { defineComponent } from 'vue' import { defineComponent } from "vue";
import draggable from 'vuedraggable' import draggable from "vuedraggable";
import ComposerCard from './ComposerCard.vue' import ComposerCard from "./ComposerCard.vue";
import { commandsWithOutput, commandsAcceptOutput } from './composerConfig' import { commandsWithOutput, commandsAcceptOutput } from "./composerConfig";
export default defineComponent({ export default defineComponent({
name: 'ComposerFlow', name: "ComposerFlow",
components: { components: {
draggable, draggable,
ComposerCard ComposerCard,
}, },
props: { props: {
modelValue: { modelValue: {
type: Array, type: Array,
required: true required: true,
} },
}, },
emits: ['update:modelValue', 'add-command'], emits: ["update:modelValue", "add-command"],
computed: { computed: {
commands: { commands: {
get() { get() {
return this.modelValue return this.modelValue;
}, },
set(value) { set(value) {
this.$emit('update:modelValue', value) this.$emit("update:modelValue", value);
} },
},
},
data() {
return {
dragIndex: -1,
isDragging: false
} }
}, },
methods: { methods: {
onDragStart() {
this.isDragging = true;
},
onDragEnd() {
this.isDragging = false;
this.dragIndex = -1;
},
onDragOver(event) {
if (!this.isDragging) {
const rect = event.currentTarget.getBoundingClientRect();
const items = this.$el.querySelectorAll('.flow-item');
const mouseY = event.clientY;
//
let closestIndex = -1;
let minDistance = Infinity;
items.forEach((item, index) => {
const itemRect = item.getBoundingClientRect();
const itemCenter = itemRect.top + itemRect.height / 2;
const distance = Math.abs(mouseY - itemCenter);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
});
//
const lastItem = items[items.length - 1];
if (lastItem && mouseY > lastItem.getBoundingClientRect().bottom) {
closestIndex = this.commands.length;
}
this.dragIndex = closestIndex;
}
},
onDragLeave() {
if (!this.isDragging) {
this.dragIndex = -1;
}
},
onDrop(event) { onDrop(event) {
const actionData = JSON.parse(event.dataTransfer.getData('action')) const actionData = JSON.parse(event.dataTransfer.getData('action'));
this.$emit('add-command', actionData) const newCommand = {
...actionData,
id: Date.now(), // 使ID
argv: '',
saveOutput: false,
useOutput: null,
cmd: actionData.value || actionData.cmd,
value: actionData.value || actionData.cmd,
};
const newCommands = [...this.commands];
if (this.dragIndex >= 0) {
newCommands.splice(this.dragIndex, 0, newCommand);
} else {
newCommands.push(newCommand);
}
this.$emit('update:modelValue', newCommands);
this.dragIndex = -1;
document.querySelectorAll('.dragging').forEach(el => { document.querySelectorAll('.dragging').forEach(el => {
el.classList.remove('dragging') el.classList.remove('dragging');
}) });
}, },
removeCommand(index) { removeCommand(index) {
const newCommands = [...this.commands] const newCommands = [...this.commands];
newCommands.splice(index, 1) newCommands.splice(index, 1);
this.$emit('update:modelValue', newCommands) this.$emit("update:modelValue", newCommands);
}, },
hasOutput(command) { hasOutput(command) {
return commandsWithOutput[command.value] || false return commandsWithOutput[command.value] || false;
}, },
canUseOutput(command, index) { canUseOutput(command, index) {
return commandsAcceptOutput[command.value] && this.getAvailableOutputs(index).length > 0 return (
commandsAcceptOutput[command.value] &&
this.getAvailableOutputs(index).length > 0
);
}, },
getAvailableOutputs(currentIndex) { getAvailableOutputs(currentIndex) {
return this.commands return this.commands
@ -99,67 +180,75 @@ export default defineComponent({
.map((cmd, index) => ({ .map((cmd, index) => ({
label: `${cmd.label} 的输出`, label: `${cmd.label} 的输出`,
value: index, value: index,
disable: !cmd.saveOutput disable: !cmd.saveOutput,
})) }))
.filter(item => !item.disable) .filter((item) => !item.disable);
}, },
toggleSaveOutput(index) { toggleSaveOutput(index) {
const newCommands = [...this.commands] const newCommands = [...this.commands];
newCommands[index].saveOutput = !newCommands[index].saveOutput newCommands[index].saveOutput = !newCommands[index].saveOutput;
if (!newCommands[index].saveOutput) { if (!newCommands[index].saveOutput) {
newCommands.forEach((cmd, i) => { newCommands.forEach((cmd, i) => {
if (i > index && cmd.useOutput === index) { if (i > index && cmd.useOutput === index) {
cmd.useOutput = null cmd.useOutput = null;
} }
}) });
} }
this.$emit('update:modelValue', newCommands) this.$emit("update:modelValue", newCommands);
}, },
handleArgvChange(index, value) { handleArgvChange(index, value) {
const newCommands = [...this.commands] const newCommands = [...this.commands];
newCommands[index].argv = value newCommands[index].argv = value;
this.$emit('update:modelValue', newCommands) this.$emit("update:modelValue", newCommands);
}, },
handleUseOutputChange(index, value) { handleUseOutputChange(index, value) {
const newCommands = [...this.commands] const newCommands = [...this.commands];
newCommands[index].useOutput = value newCommands[index].useOutput = value;
if (value !== null) { if (value !== null) {
newCommands[index].argv = '' newCommands[index].argv = "";
} }
this.$emit('update:modelValue', newCommands) this.$emit("update:modelValue", newCommands);
}, },
getPlaceholder(element, index) { getPlaceholder(element, index) {
if (element.useOutput !== null) { if (element.useOutput !== null) {
return `使用 ${this.commands[element.useOutput].label} 的输出` return `使用 ${this.commands[element.useOutput].label} 的输出`;
} }
return element.desc return element.desc;
} },
} },
}) });
</script> </script>
<style scoped> <style scoped>
.composer-flow { .composer-flow {
background-color: white;
border-radius: 8px; border-radius: 8px;
height: 100%;
} }
.command-flow-container { .command-flow-container {
min-height: 100px;
padding: 8px; padding: 8px;
background-color: #fafafa; background-color: #fafafa;
border-radius: 4px; border-radius: 4px;
transition: all 0.3s ease; transition: all 0.3s ease;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
} }
.command-flow-container:empty { .body--dark .command-flow-container {
border: 2px dashed #e0e0e0; background-color: #303132;
} }
.flow-list { .flow-list {
min-height: 50px; min-height: 50px;
} }
.drop-area {
flex: 1;
min-height: 100px;
}
.empty-flow { .empty-flow {
height: 100px; height: 100px;
display: flex; display: flex;
@ -169,6 +258,11 @@ export default defineComponent({
border-radius: 4px; border-radius: 4px;
margin: 8px 0; margin: 8px 0;
transition: all 0.3s ease; transition: all 0.3s ease;
flex: 1;
}
.body--dark .empty-flow {
border: 2px dashed #676666;
} }
.empty-flow:hover { .empty-flow:hover {
@ -176,12 +270,22 @@ export default defineComponent({
background-color: #fafafa; background-color: #fafafa;
} }
.body--dark .empty-flow:hover {
border-color: #676666;
background-color: #303132;
}
/* 拖拽时的视觉反馈 */ /* 拖拽时的视觉反馈 */
.command-flow-container.drag-over { .command-flow-container.drag-over {
background-color: #f0f4ff; background-color: #f0f4ff;
border-color: #2196f3; border-color: #2196f3;
} }
.body--dark .command-flow-container.drag-over {
background-color: #303132;
border-color: #676666;
}
/* 滑动淡出动画 */ /* 滑动淡出动画 */
.slide-fade-enter-active, .slide-fade-enter-active,
.slide-fade-leave-active { .slide-fade-leave-active {
@ -198,7 +302,88 @@ export default defineComponent({
transform: translateX(30px); transform: translateX(30px);
} }
/* 拖拽指示器基础样式 */
.flow-item::before,
.flow-item::after {
content: '';
position: absolute;
left: 12px;
right: 12px;
height: 2px;
background: linear-gradient(
90deg,
transparent,
rgba(0, 0, 0, 0.08) 10%,
rgba(0, 0, 0, 0.15) 50%,
rgba(0, 0, 0, 0.08) 90%,
transparent
);
opacity: 0;
transform: scaleX(0.95) translateY(0);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
filter: blur(0.2px);
z-index: 1;
}
.flow-item::before {
top: -1px;
}
.flow-item::after {
bottom: -1px;
}
/* 激活状态 - 插入到元素之前 */
.flow-item.insert-before::before {
opacity: 1;
transform: scaleX(1) translateY(0);
box-shadow:
0 0 10px rgba(0, 0, 0, 0.03),
0 0 4px rgba(0, 0, 0, 0.05);
}
/* 激活状态 - 插入到最后 */
.flow-item.insert-after::after {
opacity: 1;
transform: scaleX(1) translateY(0);
box-shadow:
0 0 10px rgba(0, 0, 0, 0.03),
0 0 4px rgba(0, 0, 0, 0.05);
}
/* 拖拽时的卡片效果 */
.flow-item { .flow-item {
transition: all 0.3s ease; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.flow-item.insert-before {
transform: translateY(3px);
}
.flow-item.insert-after {
transform: translateY(-3px);
}
/* 拖拽时相邻元素的间距调整 */
.flow-item.insert-before + .flow-item {
transform: translateY(3px);
}
/* 暗色模式适配 */
.body--dark .flow-item::before,
.body--dark .flow-item::after {
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.08) 10%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.08) 90%,
transparent
);
box-shadow:
0 0 10px rgba(255, 255, 255, 0.03),
0 0 4px rgba(255, 255, 255, 0.05);
} }
</style> </style>

View File

@ -2,9 +2,14 @@
<div class="composer-list"> <div class="composer-list">
<q-list bordered separator class="rounded-borders"> <q-list bordered separator class="rounded-borders">
<template v-for="category in commandCategories" :key="category.label"> <template v-for="category in commandCategories" :key="category.label">
<q-item-label header class="q-py-sm bg-grey-2"> <q-item-label header class="q-py-sm">
<div class="row items-center"> <div class="row items-center">
<q-icon :name="category.icon" color="primary" size="sm" class="q-mr-sm" /> <q-icon
:name="category.icon"
color="primary"
size="sm"
class="q-mr-sm"
/>
<span class="text-weight-medium">{{ category.label }}</span> <span class="text-weight-medium">{{ category.label }}</span>
</div> </div>
</q-item-label> </q-item-label>
@ -17,8 +22,9 @@
@dragstart="onDragStart($event, command)" @dragstart="onDragStart($event, command)"
> >
<q-item-section> <q-item-section>
<q-item-label class="text-weight-medium">{{ command.label }}</q-item-label> <q-item-label class="text-weight-medium">{{
<q-item-label caption>{{ command.desc }}</q-item-label> command.label
}}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-icon name="drag_indicator" color="grey-6" size="16px" /> <q-icon name="drag_indicator" color="grey-6" size="16px" />
@ -30,61 +36,76 @@
</template> </template>
<script> <script>
import { defineComponent } from 'vue' import { defineComponent } from "vue";
import { commandCategories } from './composerConfig' import { commandCategories } from "./composerConfig";
export default defineComponent({ export default defineComponent({
name: 'ComposerList', name: "ComposerList",
props: { props: {
commands: { commands: {
type: Array, type: Array,
required: true required: true,
} },
}, },
data() { data() {
return { return {
commandCategories commandCategories,
} };
}, },
methods: { methods: {
getCategoryCommands(category) { getCategoryCommands(category) {
return this.commands.filter(cmd => return this.commands.filter((cmd) =>
category.commands.some(catCmd => catCmd.value === cmd.value || catCmd.value === cmd.cmd) category.commands.some(
) (catCmd) => catCmd.value === cmd.value || catCmd.value === cmd.cmd
)
);
}, },
onDragStart(event, command) { onDragStart(event, command) {
event.dataTransfer.setData('action', JSON.stringify({ event.dataTransfer.setData(
...command, "action",
cmd: command.value || command.cmd JSON.stringify({
})) ...command,
event.target.classList.add('dragging') cmd: command.value || command.cmd,
event.target.addEventListener('dragend', () => { })
event.target.classList.remove('dragging') );
}, { once: true }) event.target.classList.add("dragging");
} event.target.addEventListener(
} "dragend",
}) () => {
event.target.classList.remove("dragging");
},
{ once: true }
);
},
},
});
</script> </script>
<style scoped> <style scoped>
.composer-list { .composer-list {
background-color: white; background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px; border-radius: 8px;
border-color: transparent;
} }
.command-item { .body--dark .composer-list {
border: 1px solid #e0e0e0; background-color: rgba(32, 32, 32, 0.8);
}
.command-item.q-item-type {
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px; border-radius: 4px;
margin: 4px 8px; margin: 4px 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: white; background: rgba(255, 255, 255, 0.8);
cursor: grab; cursor: grab;
font-size: 12px;
} }
.command-item:hover { .command-item.q-item-type:hover {
background-color: #f8f9fa; background-color: rgba(255, 255, 255, 0.9);
transform: translateX(4px); transform: translateX(4px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
} }
.command-item.dragging { .command-item.dragging {
@ -97,22 +118,22 @@ export default defineComponent({
min-height: 32px; min-height: 32px;
padding: 4px 12px; padding: 4px 12px;
font-size: 0.9rem; font-size: 0.9rem;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background-color: #f5f5f5; background-color: rgba(255, 255, 255, 0.9);
} }
/* 暗色模式适配 */ /* 暗色模式适配 */
.body--dark .command-item { .body--dark .command-item.q-item-type {
border-color: #424242; border-color: rgba(255, 255, 255, 0.05);
background: #1d1d1d; background: rgba(255, 255, 255, 0.05);
} }
.body--dark .command-item:hover { .body--dark .command-item.q-item-type:hover {
background-color: #2d2d2d; background-color: rgba(255, 255, 255, 0.1);
} }
.body--dark .q-item-label.header { .body--dark .q-item-label.header {
border-color: #424242; border-color: rgba(255, 255, 255, 0.05);
background-color: #2d2d2d; background-color: rgba(255, 255, 255, 0.05);
} }
</style> </style>