feat: 统一管理端弹窗与内容区交互样式

- 收敛管理端公共 Modal 链路,新增表单弹窗与普通内容弹窗包装\n- 迁移 Bot、知识库、插件、工作流、资源、MCP、数据中枢与系统管理页面级弹窗\n- 统一内容区工具栏、列表容器、导航与顶部按钮的视觉密度和交互节奏
This commit is contained in:
2026-03-06 19:58:26 +08:00
parent 76c2954a70
commit b191d1aaed
99 changed files with 3148 additions and 1623 deletions

View File

@@ -1,7 +1,7 @@
<script setup>
import { computed, ref } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import { ArrowDown, Search } from '@element-plus/icons-vue';
import {
ElButton,
ElDropdown,
@@ -14,9 +14,7 @@ import {
import { hasPermission } from '#/api/common/hasPermission.ts';
import { $t } from '#/locales';
// 定义组件属性
const props = defineProps({
// 按钮配置数组
buttons: {
type: Array,
default: () => [],
@@ -29,40 +27,43 @@ const props = defineProps({
});
},
},
// 最大显示按钮数量(不包括下拉菜单)
maxVisibleButtons: {
type: Number,
default: 3,
},
// 搜索框占位符
searchPlaceholder: {
type: String,
default: $t('common.searchPlaceholder'),
},
});
const emit = defineEmits(['search', 'button-click', 'buttonClick']);
const emit = defineEmits(['search', 'buttonClick']);
// 搜索值
const searchValue = ref('');
// 计算显示的按钮
const visibleButtons = computed(() => {
return props.buttons.slice(0, props.maxVisibleButtons);
});
// 计算下拉菜单中的按钮
const dropdownButtons = computed(() => {
const dropdownButtonsTemp = props.buttons.slice(props.maxVisibleButtons);
if (dropdownButtonsTemp.length === 0) {
return [];
}
return dropdownButtonsTemp.value.filter((action) => {
return hasPermission([action.permission]);
const filterButtonsByPermission = (buttons) => {
return buttons.filter((button) => {
return !button.permission || hasPermission([button.permission]);
});
};
const visibleButtons = computed(() => {
return filterButtonsByPermission(props.buttons).slice(
0,
props.maxVisibleButtons,
);
});
// 处理搜索
const dropdownButtons = computed(() => {
return filterButtonsByPermission(props.buttons).slice(
props.maxVisibleButtons,
);
});
const emitButtonEvent = (payload) => {
emit('buttonClick', payload);
};
const handleSearch = () => {
emit('search', searchValue.value);
};
@@ -72,9 +73,8 @@ const handleReset = () => {
emit('search', '');
};
// 处理按钮点击
const handleButtonClick = (button) => {
emit('buttonClick', {
emitButtonEvent({
type: 'button',
key: button.key,
button,
@@ -82,9 +82,8 @@ const handleButtonClick = (button) => {
});
};
// 处理下拉菜单点击
const handleDropdownClick = (button) => {
emit('buttonClick', {
emitButtonEvent({
type: 'dropdown',
key: button.key,
button,
@@ -95,34 +94,34 @@ const handleDropdownClick = (button) => {
<template>
<div class="custom-header">
<!-- 左侧搜索区域 -->
<div class="header-left">
<div class="search-container">
<div>
<ElInput
v-model="searchValue"
:placeholder="$t('common.searchPlaceholder')"
class="search-input"
@keyup.enter="handleSearch"
clearable
/>
</div>
<div>
<ElButton type="primary" auto-insert-space @click="handleSearch">
{{ $t('button.query') }}
</ElButton>
</div>
<div>
<ElButton auto-insert-space @click="handleReset">
{{ $t('button.reset') }}
</ElButton>
</div>
<div class="search-group">
<ElInput
v-model="searchValue"
:placeholder="searchPlaceholder"
class="search-input"
clearable
@keyup.enter="handleSearch"
>
<template #prefix>
<ElIcon class="search-prefix">
<Search />
</ElIcon>
</template>
</ElInput>
<ElButton type="primary" auto-insert-space @click="handleSearch">
{{ $t('button.query') }}
</ElButton>
<ElButton auto-insert-space @click="handleReset">
{{ $t('button.reset') }}
</ElButton>
</div>
</div>
<!-- 右侧按钮区域 -->
<div class="header-right">
<!-- 显示的按钮最多3个 -->
<div
v-if="visibleButtons.length > 0 || dropdownButtons.length > 0"
class="header-right"
>
<template
v-for="(button, index) in visibleButtons"
:key="button.key || index"
@@ -138,14 +137,13 @@ const handleDropdownClick = (button) => {
</ElButton>
</template>
<!-- 下拉菜单隐藏的按钮 -->
<ElDropdown
v-if="dropdownButtons.length > 0"
@command="handleDropdownClick"
>
<ElButton>
{{ $t('button.more')
}}<ElIcon class="el-icon--right"><ArrowDown /></ElIcon>
{{ $t('button.more') }}
<ElIcon class="el-icon--right"><ArrowDown /></ElIcon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
@@ -158,7 +156,7 @@ const handleDropdownClick = (button) => {
<ElIcon v-if="button.icon">
<component :is="button.icon" />
</ElIcon>
<span style="margin-left: 8px">{{ button.text }}</span>
<span class="dropdown-label">{{ button.text }}</span>
</ElDropdownItem>
</ElDropdownMenu>
</template>
@@ -168,54 +166,109 @@ const handleDropdownClick = (button) => {
</template>
<style scoped>
/* 响应式设计 */
@media (max-width: 768px) {
.custom-header {
flex-direction: column;
gap: 16px;
padding: 12px 16px;
}
.header-left,
.header-right {
justify-content: center;
width: 100%;
}
.search-container {
width: 100%;
}
.header-right {
flex-wrap: wrap;
}
}
.custom-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 100%;
}
.header-left {
.header-left,
.header-right {
display: flex;
align-items: center;
min-width: 0;
}
.search-container {
.header-left {
flex: 1;
}
.search-group {
display: flex;
flex: 1;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.search-input {
width: 300px;
border-radius: 4px;
width: min(360px, 100%);
}
.search-input :deep(.el-input__wrapper) {
min-height: 40px;
background: hsl(var(--surface-contrast-soft) / 0.92);
border-radius: 14px;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.44),
0 10px 24px -24px hsl(var(--foreground) / 0.24);
}
.search-input :deep(.el-input__wrapper:hover),
.search-input :deep(.el-input__wrapper.is-focus) {
background: hsl(var(--surface-subtle) / 0.98);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.56),
0 0 0 3px hsl(var(--primary) / 0.08),
0 12px 24px -24px hsl(var(--foreground) / 0.24);
}
.search-prefix {
color: hsl(var(--text-muted));
}
.header-right {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
align-items: center;
}
.header-right :deep(.el-button),
.search-group :deep(.el-button) {
min-width: 64px;
height: 32px;
min-height: 32px;
padding-inline: 18px;
border-radius: 10px;
border-color: transparent;
box-shadow: none;
}
.header-right :deep(.el-button:not(.el-button--primary)),
.search-group :deep(.el-button:not(.el-button--primary)) {
color: hsl(var(--text-strong));
background: hsl(var(--surface-contrast-soft) / 0.86);
border-color: transparent;
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.34),
0 10px 24px -24px hsl(var(--foreground) / 0.2);
}
.header-right :deep(.el-button--primary),
.search-group :deep(.el-button--primary) {
box-shadow: 0 16px 28px -22px hsl(var(--primary) / 0.48);
}
.dropdown-label {
margin-left: 8px;
}
@media (max-width: 768px) {
.custom-header {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.header-right {
justify-content: flex-start;
}
.search-input {
width: 100%;
}
}
</style>