feat: 统一管理端弹窗与内容区交互样式
- 收敛管理端公共 Modal 链路,新增表单弹窗与普通内容弹窗包装\n- 迁移 Bot、知识库、插件、工作流、资源、MCP、数据中枢与系统管理页面级弹窗\n- 统一内容区工具栏、列表容器、导航与顶部按钮的视觉密度和交互节奏
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user