feat: 增强命令面板功能与AI助手集成
This commit is contained in:
@@ -119,10 +119,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, watch } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import AiCommandApi from "@/api/ai";
|
import AiCommandApi from "@/api/ai";
|
||||||
|
import { useSettingsStore } from "@/store";
|
||||||
|
|
||||||
type ToolFunctionCall = {
|
type ToolFunctionCall = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -156,6 +157,7 @@ type AiResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
@@ -188,7 +190,7 @@ const getActiveRightDrawerWidth = (): number => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const rect = drawer.getBoundingClientRect();
|
const rect = drawer.getBoundingClientRect();
|
||||||
if (rect.width > 0 && rect.right >= window.innerWidth - 1) {
|
if (rect.width > 0 && rect.right >= window.innerWidth - 8) {
|
||||||
return rect.width;
|
return rect.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,6 +257,16 @@ watch(
|
|||||||
{ flush: "post" }
|
{ flush: "post" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => settingsStore.settingsVisible,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
scheduleUpdateFabPositionBurst();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ flush: "post" }
|
||||||
|
);
|
||||||
|
|
||||||
let domObserver: MutationObserver | null = null;
|
let domObserver: MutationObserver | null = null;
|
||||||
let rafId: number | null = null;
|
let rafId: number | null = null;
|
||||||
|
|
||||||
@@ -268,6 +280,18 @@ const scheduleUpdateFabPosition = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scheduleUpdateFabPositionBurst = (frames = 18) => {
|
||||||
|
let count = 0;
|
||||||
|
const tick = () => {
|
||||||
|
scheduleUpdateFabPosition();
|
||||||
|
count += 1;
|
||||||
|
if (count < frames) {
|
||||||
|
window.requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
};
|
||||||
|
|
||||||
// 快捷命令示例
|
// 快捷命令示例
|
||||||
const examples = [
|
const examples = [
|
||||||
"修改test用户的姓名为测试人员",
|
"修改test用户的姓名为测试人员",
|
||||||
|
|||||||
@@ -1,66 +1,294 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-button type="text" icon="Search" aria-label="打开搜索面板" @click="open" />
|
<div
|
||||||
|
class="command-palette-trigger"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="打开搜索面板"
|
||||||
|
@click="open"
|
||||||
|
@keydown.enter.prevent="open"
|
||||||
|
@keydown.space.prevent="open"
|
||||||
|
>
|
||||||
|
<div class="command-palette-trigger__left">
|
||||||
|
<div class="i-svg:search" />
|
||||||
|
<span class="command-palette-trigger__text">搜索菜单</span>
|
||||||
|
</div>
|
||||||
|
<kbd class="command-palette-trigger__kbd">Ctrl K</kbd>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="visible" width="640px" :close-on-click-modal="true" @close="close">
|
<el-dialog
|
||||||
<template #title>
|
v-model="visible"
|
||||||
<div class="flex items-center gap-2">
|
width="720px"
|
||||||
<el-icon><Search /></el-icon>
|
:close-on-click-modal="true"
|
||||||
<span>搜索菜单</span>
|
:show-close="false"
|
||||||
</div>
|
@close="close"
|
||||||
</template>
|
>
|
||||||
|
<div class="command-palette-dialog">
|
||||||
<div>
|
|
||||||
<el-input
|
<el-input
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="输入菜单名称或关键字搜索"
|
class="command-palette-input"
|
||||||
|
placeholder="搜索菜单"
|
||||||
@input="onSearch"
|
@input="onSearch"
|
||||||
@keydown.enter.prevent="onSelect"
|
@keydown="handleInputKeydown"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<div class="i-svg:search" />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<div class="command-palette-input__suffix">
|
||||||
|
<div
|
||||||
|
class="i-svg:close"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="关闭"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="command-palette-results">
|
||||||
<div
|
<div v-if="displayList.length === 0" class="command-palette-empty">没有搜索历史</div>
|
||||||
v-if="results.length === 0 && history.length === 0"
|
|
||||||
class="text-center text-gray-500"
|
|
||||||
>
|
|
||||||
没有搜索历史
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul v-else class="space-y-2">
|
<ul v-else class="command-palette-list">
|
||||||
<li
|
<li
|
||||||
v-for="(item, idx) in results.length ? results : history"
|
v-for="(item, idx) in displayList"
|
||||||
:key="item.path + idx"
|
:key="item.path + idx"
|
||||||
class="p-2 hover:bg-gray-100 cursor-pointer rounded"
|
:class="['command-palette-item', { 'is-active': activeIndex === idx }]"
|
||||||
|
@mouseenter="activeIndex = idx"
|
||||||
@click="onGo(item)"
|
@click="onGo(item)"
|
||||||
>
|
>
|
||||||
<div>{{ item.title }}</div>
|
<div class="command-palette-item__title">{{ item.title }}</div>
|
||||||
<div class="text-sm text-gray-400">{{ item.path }}</div>
|
<div class="command-palette-item__path">{{ item.path }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
<div class="command-palette-hints">
|
||||||
<div style="text-align: right">
|
<div class="command-palette-hint">
|
||||||
<el-button @click="close">关闭</el-button>
|
<div class="command-palette-hint__key"><div class="i-svg:up" /></div>
|
||||||
|
<div class="command-palette-hint__key"><div class="i-svg:down" /></div>
|
||||||
|
<span class="command-palette-hint__text">切换</span>
|
||||||
|
</div>
|
||||||
|
<div class="command-palette-hint">
|
||||||
|
<div class="command-palette-hint__key"><div class="i-svg:enter" /></div>
|
||||||
|
<span class="command-palette-hint__text">选择</span>
|
||||||
|
</div>
|
||||||
|
<div class="command-palette-hint">
|
||||||
|
<div class="command-palette-hint__key"><div class="i-svg:esc" /></div>
|
||||||
|
<span class="command-palette-hint__text">关闭</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { computed } from "vue";
|
||||||
import { Search } from "@element-plus/icons-vue";
|
|
||||||
import { useCommandPalette } from "./useCommandPalette";
|
import { useCommandPalette } from "./useCommandPalette";
|
||||||
|
|
||||||
const { visible, keyword, results, history, inputRef, open, close, onSearch, onSelect, onGo } =
|
const {
|
||||||
useCommandPalette();
|
visible,
|
||||||
|
keyword,
|
||||||
|
results,
|
||||||
|
history,
|
||||||
|
activeIndex,
|
||||||
|
inputRef,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
onSearch,
|
||||||
|
onSelect,
|
||||||
|
onNavigate,
|
||||||
|
onGo,
|
||||||
|
} = useCommandPalette();
|
||||||
|
|
||||||
onMounted(() => {
|
const displayList = computed(() => (results.value.length ? results.value : history.value));
|
||||||
// no-op for now
|
|
||||||
});
|
const handleInputKeydown: (evt: KeyboardEvent | Event) => any = (evt) => {
|
||||||
|
if (!(evt instanceof KeyboardEvent)) return;
|
||||||
|
const e = evt;
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
|
||||||
|
if (key === "escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "arrowup") {
|
||||||
|
e.preventDefault();
|
||||||
|
onNavigate("up");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "arrowdown") {
|
||||||
|
e.preventDefault();
|
||||||
|
onNavigate("down");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (displayList.value.length === 0) return;
|
||||||
|
if (activeIndex.value < 0) activeIndex.value = 0;
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.command-palette-trigger {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
user-select: none;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-trigger__left {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-trigger__text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-trigger__kbd {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-trigger:focus-visible {
|
||||||
|
outline: 2px solid var(--el-color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-trigger:hover {
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-input :deep(.el-input__wrapper) {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-input__suffix {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-input__suffix :deep([class^="i-svg:"]) {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-input__suffix :deep([class^="i-svg:"]):hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-results {
|
||||||
|
max-height: 48vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-empty {
|
||||||
|
padding: 24px 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-item:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-item.is-active {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-item__title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-item__path {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-hints {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-hint__key {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-hint__key :deep([class^="i-svg:"]) {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-hint__text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -65,19 +65,27 @@ export function useCommandPalette() {
|
|||||||
results.value = menuItems.value.filter((item) => item.title.toLowerCase().includes(kw));
|
results.value = menuItems.value.filter((item) => item.title.toLowerCase().includes(kw));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayList() {
|
||||||
|
return results.value.length ? results.value : history.value;
|
||||||
|
}
|
||||||
|
|
||||||
function onSelect() {
|
function onSelect() {
|
||||||
if (results.value.length > 0 && activeIndex.value >= 0) {
|
const list = getDisplayList();
|
||||||
onGo(results.value[activeIndex.value]);
|
if (list.length === 0) return;
|
||||||
}
|
if (activeIndex.value < 0) return;
|
||||||
|
const item = list[activeIndex.value];
|
||||||
|
if (!item) return;
|
||||||
|
onGo(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNavigate(direction: "up" | "down") {
|
function onNavigate(direction: "up" | "down") {
|
||||||
if (results.value.length === 0) return;
|
const list = getDisplayList();
|
||||||
|
if (list.length === 0) return;
|
||||||
|
|
||||||
if (direction === "up") {
|
if (direction === "up") {
|
||||||
activeIndex.value = activeIndex.value <= 0 ? results.value.length - 1 : activeIndex.value - 1;
|
activeIndex.value = activeIndex.value <= 0 ? list.length - 1 : activeIndex.value - 1;
|
||||||
} else {
|
} else {
|
||||||
activeIndex.value = activeIndex.value >= results.value.length - 1 ? 0 : activeIndex.value + 1;
|
activeIndex.value = activeIndex.value >= list.length - 1 ? 0 : activeIndex.value + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ export const enum ApiCodeEnum {
|
|||||||
* 成功
|
* 成功
|
||||||
*/
|
*/
|
||||||
SUCCESS = "00000",
|
SUCCESS = "00000",
|
||||||
/**
|
|
||||||
* 错误
|
|
||||||
*/
|
|
||||||
ERROR = "B0001",
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 访问令牌无效或过期
|
* 访问令牌无效或过期
|
||||||
|
|||||||
@@ -49,12 +49,14 @@
|
|||||||
<div class="i-svg:captcha" />
|
<div class="i-svg:captcha" />
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div cursor-pointer h-40px w-120px flex-center @click="getCaptcha">
|
<div cursor-pointer h-44px w-140px flex-center @click="getCaptcha">
|
||||||
<el-icon v-if="codeLoading" class="is-loading" size="20"><Loading /></el-icon>
|
<el-icon v-if="codeLoading" class="is-loading" size="20"><Loading /></el-icon>
|
||||||
<img
|
<img
|
||||||
v-else-if="captchaBase64"
|
v-else-if="captchaBase64"
|
||||||
border-rd-4px
|
border-rd-4px
|
||||||
object-cover
|
w-full
|
||||||
|
h-full
|
||||||
|
object-contain
|
||||||
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
|
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
|
||||||
:src="captchaBase64"
|
:src="captchaBase64"
|
||||||
alt="captchaCode"
|
alt="captchaCode"
|
||||||
|
|||||||
@@ -58,14 +58,15 @@
|
|||||||
<div class="i-svg:captcha" />
|
<div class="i-svg:captcha" />
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div cursor-pointer h="[40px]" w="[120px]" flex-center ml-10px @click="getCaptcha">
|
<div cursor-pointer h="[44px]" w="[140px]" flex-center ml-10px @click="getCaptcha">
|
||||||
<el-icon v-if="codeLoading" class="is-loading"><Loading /></el-icon>
|
<el-icon v-if="codeLoading" class="is-loading"><Loading /></el-icon>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
object-cover
|
|
||||||
border-rd-4px
|
border-rd-4px
|
||||||
p-1px
|
w-full
|
||||||
|
h-full
|
||||||
|
object-contain
|
||||||
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
|
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
|
||||||
:src="captchaBase64"
|
:src="captchaBase64"
|
||||||
alt="code"
|
alt="code"
|
||||||
|
|||||||
Reference in New Issue
Block a user