编排的命令列表支持搜索

This commit is contained in:
fofolee 2024-12-30 10:31:23 +08:00
parent dfc1c7e2e3
commit 29d7064afc
11 changed files with 272 additions and 144 deletions

View File

@ -3,33 +3,18 @@
<!-- 主体内容 --> <!-- 主体内容 -->
<div class="composer-body row no-wrap"> <div class="composer-body row no-wrap">
<!-- 左侧命令列表 --> <!-- 左侧命令列表 -->
<div class="col-3 command-section command-list"> <div class="col-3 command-section">
<!-- <div class="section-header"> <ComposerList :commands="availableCommands" @add-command="addCommand" />
<q-icon name="list" size="20px" class="q-mr-sm text-primary" />
<span class="text-subtitle1">可用命令</span>
</div> -->
<q-scroll-area class="command-scroll">
<ComposerList
:commands="availableCommands"
@add-command="addCommand"
/>
</q-scroll-area>
</div> </div>
<!-- 右侧命令流程 --> <!-- 右侧命令流程 -->
<div class="col command-section command-flow"> <div class="col command-section">
<div class="section-header"> <ComposerFlow
<q-icon name="timeline" size="20px" class="q-mr-sm text-primary" /> v-model="commandFlow"
<span class="text-subtitle1">命令流程</span> :generate-code="generateFlowCode"
<q-space /> @add-command="addCommand"
<ComposerButtons @action="handleComposer"
:generate-code="generateFlowCode" />
@action="handleComposer"
/>
</div>
<q-scroll-area class="command-scroll">
<ComposerFlow v-model="commandFlow" @add-command="addCommand" />
</q-scroll-area>
</div> </div>
</div> </div>
</div> </div>
@ -39,7 +24,6 @@
import { defineComponent, provide, ref } from "vue"; import { defineComponent, provide, ref } from "vue";
import ComposerList from "./ComposerList.vue"; import ComposerList from "./ComposerList.vue";
import ComposerFlow from "./ComposerFlow.vue"; import ComposerFlow from "./ComposerFlow.vue";
import ComposerButtons from "./ComposerButtons.vue";
import { commandCategories } from "js/composer/composerConfig"; import { commandCategories } from "js/composer/composerConfig";
import { generateCode } from "js/composer/generateCode"; import { generateCode } from "js/composer/generateCode";
// commandCategories // commandCategories
@ -57,7 +41,6 @@ export default defineComponent({
components: { components: {
ComposerList, ComposerList,
ComposerFlow, ComposerFlow,
ComposerButtons,
}, },
setup() { setup() {
const variables = ref([]); const variables = ref([]);
@ -154,25 +137,6 @@ export default defineComponent({
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
} }
.section-header {
display: flex;
align-items: center;
padding: 12px 16px;
height: 28px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.body--dark .section-header {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.command-scroll {
flex: 1;
overflow: hidden;
border-radius: 8px;
padding-bottom: 8px;
}
/* 滚动美化 */ /* 滚动美化 */
:deep(.q-scrollarea__thumb) { :deep(.q-scrollarea__thumb) {
width: 2px; width: 2px;

View File

@ -98,7 +98,7 @@ export default defineComponent({
.composer-buttons > .q-btn:hover { .composer-buttons > .q-btn:hover {
opacity: 1; opacity: 1;
transform: translateY(-2px); transform: translateY(-1px);
color: var(--q-primary); color: var(--q-primary);
} }

View File

@ -1,55 +1,67 @@
<template> <template>
<div class="composer-flow"> <div class="composer-flow">
<div <div class="section-header">
class="command-flow-container" <q-icon name="timeline" size="20px" class="q-mx-sm text-primary" />
@dragover.prevent="onDragOver" <span class="text-subtitle1">命令流程</span>
@drop="onDrop" <q-space />
@dragleave.prevent="onDragLeave" <ComposerButtons
> :generate-code="generateCode"
<draggable @action="$emit('action', $event)"
v-model="commands" />
group="commands" </div>
item-key="id"
class="flow-list" <q-scroll-area class="command-scroll">
handle=".drag-handle" <div
:animation="200" class="command-flow-container"
@start="onDragStart" @dragover.prevent="onDragOver"
@end="onDragEnd" @drop="onDrop"
@dragleave.prevent="onDragLeave"
> >
<template #item="{ element, index }"> <draggable
<transition name="slide-fade" mode="out-in" appear> v-model="commands"
<div group="commands"
:key="element.id" item-key="id"
class="flow-item" class="flow-list"
:class="{ handle=".drag-handle"
'insert-before': dragIndex === index, :animation="200"
'insert-after': @start="onDragStart"
dragIndex === commands.length && @end="onDragEnd"
index === commands.length - 1, >
}" <template #item="{ element, index }">
> <transition name="slide-fade" mode="out-in" appear>
<ComposerCard <div
:command="element" :key="element.id"
:placeholder="getPlaceholder(element, index)" class="flow-item"
@remove="removeCommand(index)" :class="{
@toggle-output="toggleSaveOutput(index)" 'insert-before': dragIndex === index,
@update:argv="(val) => handleArgvChange(index, val)" 'insert-after':
@update:command="(val) => updateCommand(index, val)" dragIndex === commands.length &&
/> index === commands.length - 1,
</div> }"
</transition> >
</template> <ComposerCard
</draggable> :command="element"
<div v-if="commands.length === 0" class="empty-flow"> :placeholder="getPlaceholder(element, index)"
<div class="text-center text-grey-6"> @remove="removeCommand(index)"
<q-icon name="drag_indicator" size="32px" /> @toggle-output="toggleSaveOutput(index)"
<div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div> @update:argv="(val) => handleArgvChange(index, val)"
@update:command="(val) => updateCommand(index, val)"
/>
</div>
</transition>
</template>
</draggable>
<div v-if="commands.length === 0" class="empty-flow">
<div class="text-center text-grey-6">
<q-icon name="drag_indicator" size="32px" />
<div class="text-body2 q-mt-sm">从左侧拖拽命令到这里开始编排</div>
</div>
</div>
<div v-else class="drop-area">
<q-icon name="add" size="32px" />
</div> </div>
</div> </div>
<div v-else class="drop-area"> </q-scroll-area>
<q-icon name="add" size="32px" />
</div>
</div>
</div> </div>
</template> </template>
@ -57,20 +69,26 @@
import { defineComponent, inject } from "vue"; import { defineComponent, inject } from "vue";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import ComposerCard from "./ComposerCard.vue"; import ComposerCard from "./ComposerCard.vue";
import ComposerButtons from "./ComposerButtons.vue";
export default defineComponent({ export default defineComponent({
name: "ComposerFlow", name: "ComposerFlow",
components: { components: {
draggable, draggable,
ComposerCard, ComposerCard,
ComposerButtons,
}, },
props: { props: {
modelValue: { modelValue: {
type: Array, type: Array,
required: true, required: true,
}, },
generateCode: {
type: Function,
required: true,
},
}, },
emits: ["update:modelValue", "add-command"], emits: ["update:modelValue", "add-command", "action"],
computed: { computed: {
commands: { commands: {
get() { get() {
@ -230,8 +248,25 @@ export default defineComponent({
<style scoped> <style scoped>
.composer-flow { .composer-flow {
border-radius: 8px; display: flex;
flex-direction: column;
height: 100%; height: 100%;
border-radius: 10px;
}
.section-header {
flex-shrink: 0;
padding: 0 8px;
height: 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
}
.command-scroll {
flex: 1;
overflow: hidden;
border-radius: 10px;
} }
.command-flow-container { .command-flow-container {
@ -239,7 +274,6 @@ export default defineComponent({
background-color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.8);
border-radius: 4px; border-radius: 4px;
transition: all 0.3s ease; transition: all 0.3s ease;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
@ -391,4 +425,8 @@ export default defineComponent({
box-shadow: 0 0 10px rgba(255, 255, 255, 0.03), box-shadow: 0 0 10px rgba(255, 255, 255, 0.03),
0 0 4px rgba(255, 255, 255, 0.05); 0 0 4px rgba(255, 255, 255, 0.05);
} }
.body--dark .section-header {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
</style> </style>

View File

@ -1,43 +1,75 @@
<template> <template>
<div class="composer-list"> <div class="composer-list">
<q-list separator class="rounded-borders"> <div class="section-header">
<template v-for="category in commandCategories" :key="category.label"> <q-input
<q-item-label header class="q-py-sm"> v-model="searchQuery"
<div class="row items-center"> dense
<q-icon borderless
:name="category.icon" placeholder="搜索命令..."
color="primary" class="search-input"
size="sm" >
class="q-mr-sm" <template v-slot:prepend>
/> <q-icon name="search" size="sm" />
<span class="text-weight-medium">{{ category.label }}</span> </template>
</div> </q-input>
</q-item-label> </div>
<q-item <q-scroll-area class="command-scroll">
v-for="command in getCategoryCommands(category)" <div>
:key="command.value" <q-list separator class="rounded-borders">
class="command-item q-py-xs" <template
draggable="true" v-for="category in filteredCategories"
@dragstart="onDragStart($event, command)" :key="category.label"
> >
<q-item-section> <q-expansion-item
<q-item-label class="text-weight-medium">{{ :label="category.label"
command.label :icon="category.icon"
}}</q-item-label> :default-opened="!!searchQuery || category.defaultOpened"
</q-item-section> dense-toggle
<q-item-section side style="padding-left: 8px"> class="category-item"
<q-icon name="drag_indicator" color="grey-6" size="16px" /> header-class="category-header"
</q-item-section> >
</q-item> <template v-slot:header>
</template> <div class="row items-center">
</q-list> <q-icon
:name="category.icon"
color="primary"
size="sm"
class="q-mr-sm"
/>
<span class="text-weight-medium">{{ category.label }}</span>
</div>
</template>
<q-item
v-for="command in getCategoryCommands(category)"
:key="command.value"
class="command-item q-py-xs"
draggable="true"
@dragstart="onDragStart($event, command)"
>
<q-item-section>
<q-item-label
class="text-weight-medium"
v-html="highlightText(command.label)"
/>
</q-item-section>
<q-item-section side style="padding-left: 8px">
<q-icon name="drag_indicator" color="grey-6" size="16px" />
</q-item-section>
</q-item>
</q-expansion-item>
</template>
</q-list>
</div>
</q-scroll-area>
</div> </div>
</template> </template>
<script> <script>
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { commandCategories } from "js/composer/composerConfig"; import { commandCategories } from "js/composer/composerConfig";
import pinyinMatch from "pinyin-match";
export default defineComponent({ export default defineComponent({
name: "ComposerList", name: "ComposerList",
@ -50,8 +82,33 @@ export default defineComponent({
data() { data() {
return { return {
commandCategories, commandCategories,
searchQuery: "",
}; };
}, },
computed: {
filteredCategories() {
if (!this.searchQuery) return this.commandCategories;
const query = this.searchQuery.toLowerCase();
return this.commandCategories
.map((category) => ({
...category,
commands: this.commands
.filter(
(cmd) =>
(cmd.label && pinyinMatch.match(cmd.label, query)) ||
(cmd.value && pinyinMatch.match(cmd.value, query))
)
.filter((cmd) =>
category.commands.some(
(catCmd) =>
catCmd.value === cmd.value || catCmd.value === cmd.cmd
)
),
}))
.filter((category) => category.commands.length > 0);
},
},
methods: { methods: {
getCategoryCommands(category) { getCategoryCommands(category) {
return this.commands.filter((cmd) => return this.commands.filter((cmd) =>
@ -77,33 +134,98 @@ export default defineComponent({
{ once: true } { once: true }
); );
}, },
highlightText(text) {
if (!this.searchQuery) return text;
const matches = pinyinMatch.match(text, this.searchQuery);
if (!matches) return text;
const [start, end] = matches;
return (
text.slice(0, start) +
`<span class="highlight">${text.slice(start, end + 1)}</span>` +
text.slice(end + 1)
);
},
}, },
}); });
</script> </script>
<style scoped> <style scoped>
.composer-list { .composer-list {
background-color: rgba(255, 255, 255, 0.8); display: flex;
border-radius: 8px; flex-direction: column;
border-color: transparent; height: 100%;
border-radius: 10px;
}
.section-header {
flex-shrink: 0;
padding: 0 8px;
height: 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.command-scroll {
flex: 1;
overflow: hidden;
border-radius: 10px;
}
.search-input {
width: 100%;
}
.search-input :deep(.q-field__control) {
height: 100%;
padding: 0 4px;
border-radius: 10px;
}
.search-input :deep(.q-field__native) {
padding: 0;
font-size: 12px;
}
.search-input :deep(.q-field__marginal) {
height: 100%;
}
.composer-list :deep(.q-expansion-item) {
margin: 0;
border: none;
} }
.body--dark .composer-list { .body--dark .composer-list {
background-color: rgba(32, 32, 32, 0.8); background-color: rgba(32, 32, 32, 0.8);
} }
.category-item {
margin: 4px 0;
border-radius: 4px;
overflow: hidden;
}
:deep(.q-item.category-header) {
min-height: 40px;
margin: 0 8px;
padding: 0 4px;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.command-item.q-item-type { .command-item.q-item-type {
border: 1px solid rgba(0, 0, 0, 0.05); 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: rgba(255, 255, 255, 0.8); background: rgba(0, 0, 0, 0.02);
cursor: grab; cursor: grab;
font-size: 12px; font-size: 12px;
} }
.command-item.q-item-type:hover { .command-item.q-item-type:hover {
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(0, 0, 0, 0.1);
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);
} }
@ -113,15 +235,6 @@ export default defineComponent({
transform: scale(0.95); transform: scale(0.95);
} }
/* 分类标题样式 */
.q-item-label.header {
min-height: 32px;
padding: 4px 12px;
font-size: 0.9rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background-color: rgba(255, 255, 255, 0.9);
}
/* 暗色模式适配 */ /* 暗色模式适配 */
.body--dark .command-item.q-item-type { .body--dark .command-item.q-item-type {
border-color: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.05);
@ -132,8 +245,14 @@ export default defineComponent({
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
.body--dark .q-item-label.header { .body--dark .section-header {
border-color: rgba(255, 255, 255, 0.05); border-bottom-color: rgba(255, 255, 255, 0.05);
background-color: rgba(255, 255, 255, 0.05); }
.command-item :deep(.highlight) {
color: var(--q-primary);
font-weight: bold;
padding: 0 1px;
border-radius: 2px;
} }
</style> </style>

View File

@ -1,6 +1,7 @@
export const encodeCommands = { export const encodeCommands = {
label: "编码解码", label: "编码解码",
icon: "code", icon: "code",
defaultOpened: false,
commands: [ commands: [
{ {
value: "(text=>Buffer.from(text).toString('base64'))", value: "(text=>Buffer.from(text).toString('base64'))",

View File

@ -1,6 +1,7 @@
export const fileCommands = { export const fileCommands = {
label: "文件操作", label: "文件操作",
icon: "folder", icon: "folder",
defaultOpened: true,
commands: [ commands: [
{ {
value: "open", value: "open",

View File

@ -1,6 +1,7 @@
export const keyCommands = { export const keyCommands = {
label: "按键操作", label: "按键操作",
icon: "keyboard", icon: "keyboard",
defaultOpened: false,
commands: [ commands: [
{ {
value: "keyTap", value: "keyTap",

View File

@ -1,6 +1,7 @@
export const networkCommands = { export const networkCommands = {
label: "网络操作", label: "网络操作",
icon: "language", icon: "language",
defaultOpened: true,
commands: [ commands: [
{ {
value: "visit", value: "visit",

View File

@ -1,6 +1,7 @@
export const notifyCommands = { export const notifyCommands = {
label: "消息通知", label: "消息通知",
icon: "notifications", icon: "notifications",
defaultOpened: false,
commands: [ commands: [
{ {
value: "console.log", value: "console.log",

View File

@ -1,6 +1,7 @@
export const otherCommands = { export const otherCommands = {
label: "其他功能", label: "其他功能",
icon: "more_horiz", icon: "more_horiz",
defaultOpened: false,
commands: [ commands: [
{ {
value: "utools.redirect", value: "utools.redirect",

View File

@ -1,6 +1,7 @@
export const systemCommands = { export const systemCommands = {
label: "系统操作", label: "系统操作",
icon: "computer", icon: "computer",
defaultOpened: false,
commands: [ commands: [
{ {
value: "system", value: "system",