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>
|
||||
|
||||
227
easyflow-ui-admin/app/src/components/page/ListPageShell.vue
Normal file
227
easyflow-ui-admin/app/src/components/page/ListPageShell.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed, useSlots } from 'vue';
|
||||
|
||||
interface Props {
|
||||
contentPadding?: number | string;
|
||||
dense?: boolean;
|
||||
stickyToolbar?: boolean;
|
||||
surface?: 'panel' | 'subtle';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
contentPadding: 0,
|
||||
dense: false,
|
||||
stickyToolbar: false,
|
||||
surface: 'panel',
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const hasToolbar = computed(() => Boolean(slots.filters || slots.actions));
|
||||
|
||||
const contentStyle = computed((): CSSProperties => {
|
||||
const padding =
|
||||
typeof props.contentPadding === 'number'
|
||||
? `${props.contentPadding}px`
|
||||
: props.contentPadding;
|
||||
|
||||
return padding ? { '--list-page-shell-content-padding': padding } : {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
:class="[
|
||||
'list-page-shell',
|
||||
`is-${surface}`,
|
||||
{
|
||||
'is-dense': dense,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="hasToolbar"
|
||||
:class="[
|
||||
'list-page-shell__toolbar',
|
||||
{
|
||||
'is-sticky': stickyToolbar,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div v-if="$slots.filters" class="list-page-shell__filters">
|
||||
<slot name="filters"></slot>
|
||||
</div>
|
||||
<div v-if="$slots.actions" class="list-page-shell__actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-page-shell__content" :style="contentStyle">
|
||||
<slot></slot>
|
||||
<slot name="empty"></slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-page-shell {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.list-page-shell.is-dense {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-page-shell__toolbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 8px 6px 2px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.list-page-shell__toolbar.is-sticky {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
z-index: 5;
|
||||
padding: 10px 10px 6px;
|
||||
background: hsl(var(--glass-tint) / 0.7);
|
||||
border: 1px solid hsl(var(--glass-border) / 0.44);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow-toolbar);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(155%);
|
||||
}
|
||||
|
||||
.list-page-shell__filters,
|
||||
.list-page-shell__actions {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-page-shell__filters {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-page-shell__content {
|
||||
--list-page-shell-content-padding: 0px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: var(--list-page-shell-content-padding);
|
||||
background: hsl(var(--surface-panel));
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow-subtle);
|
||||
}
|
||||
|
||||
.list-page-shell.is-subtle .list-page-shell__content {
|
||||
background: hsl(var(--surface-contrast-soft));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.page-data-container) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-empty) {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table) {
|
||||
--el-table-bg-color: transparent;
|
||||
--el-table-tr-bg-color: transparent;
|
||||
--el-table-header-bg-color: hsl(var(--table-header-bg));
|
||||
--el-table-border-color: hsl(var(--table-row-border));
|
||||
--el-table-current-row-bg-color: hsl(var(--table-row-hover));
|
||||
--el-fill-color-blank: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table--border),
|
||||
.list-page-shell__content :deep(.el-table__inner-wrapper),
|
||||
.list-page-shell__content :deep(.el-table__header-wrapper),
|
||||
.list-page-shell__content :deep(.el-table__body-wrapper) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table__inner-wrapper::before),
|
||||
.list-page-shell__content :deep(.el-table--border::before),
|
||||
.list-page-shell__content :deep(.el-table--border::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table th.el-table__cell) {
|
||||
padding: 12px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--table-header-bg) / 0.86);
|
||||
border-bottom: 1px solid hsl(var(--divider-faint) / 0.38);
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table th.el-table__cell:first-child) {
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table th.el-table__cell:last-child) {
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table td.el-table__cell),
|
||||
.list-page-shell__content :deep(.el-table--border .el-table__cell) {
|
||||
padding: 14px 0;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid hsl(var(--divider-faint) / 0.46);
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-table__row:hover > td.el-table__cell) {
|
||||
background: hsl(var(--table-row-hover)) !important;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-tag) {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-pagination) {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.list-page-shell__content :deep(.el-pagination button),
|
||||
.list-page-shell__content :deep(.el-pager li) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.list-page-shell__toolbar :deep(.custom-header) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.list-page-shell {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-page-shell__toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 2px 0 0;
|
||||
}
|
||||
|
||||
.list-page-shell__actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@ const props = withDefaults(defineProps<PageDataProps>(), {
|
||||
// 响应式数据
|
||||
const pageList = ref([]);
|
||||
const loading = ref(false);
|
||||
const queryParams = ref({});
|
||||
const queryParams = ref<Record<string, any>>({});
|
||||
|
||||
const pageInfo = reactive({
|
||||
pageNumber: 1,
|
||||
@@ -32,7 +32,7 @@ const pageInfo = reactive({
|
||||
});
|
||||
|
||||
// 模拟 API 调用 - 这里需要根据你的实际 API 调用方式调整
|
||||
const doGet = async (params: any) => {
|
||||
const doGet = async (params: Record<string, any>) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 这里替换为你的实际 API 调用
|
||||
@@ -76,7 +76,7 @@ const handleCurrentChange = (newPage: number) => {
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法 (替代 useImperativeHandle)
|
||||
const setQuery = (newQueryParams: string) => {
|
||||
const setQuery = (newQueryParams: Record<string, any>) => {
|
||||
pageInfo.pageNumber = 1;
|
||||
pageInfo.pageSize = props.pageSize;
|
||||
queryParams.value = newQueryParams;
|
||||
@@ -85,6 +85,7 @@ const setQuery = (newQueryParams: string) => {
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
reload: getPageList,
|
||||
setQuery,
|
||||
});
|
||||
|
||||
@@ -106,10 +107,13 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="page-data-container" v-loading="loading">
|
||||
<template v-if="pageList.length > 0">
|
||||
<div>
|
||||
<div class="page-data-container__body">
|
||||
<slot :page-list="pageList"></slot>
|
||||
</div>
|
||||
<div v-if="pageInfo.total > pageInfo.pageSize" class="mx-auto mt-8 w-fit">
|
||||
<div
|
||||
v-if="pageInfo.total > pageInfo.pageSize"
|
||||
class="page-data-container__pagination mx-auto mt-6 w-fit"
|
||||
>
|
||||
<ElPagination
|
||||
v-model:current-page="pageInfo.pageNumber"
|
||||
v-model:page-size="pageInfo.pageSize"
|
||||
@@ -121,9 +125,36 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
v-else
|
||||
/>
|
||||
<slot v-else name="empty">
|
||||
<ElEmpty
|
||||
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-data-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 18px 20px 18px;
|
||||
}
|
||||
|
||||
.page-data-container__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.page-data-container__pagination {
|
||||
flex-shrink: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-data-container {
|
||||
padding: 14px 14px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,6 +25,18 @@ const props = defineProps({
|
||||
default: () => ['image/gif', 'image/jpeg', 'image/png', 'image/webp'],
|
||||
},
|
||||
modelValue: { type: String, default: '' },
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['success', 'update:modelValue']);
|
||||
@@ -77,6 +89,11 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
<template>
|
||||
<ElUpload
|
||||
class="avatar-uploader"
|
||||
:class="[
|
||||
`avatar-uploader--${props.theme}`,
|
||||
{ 'avatar-uploader--rounded': props.shape === 'rounded' },
|
||||
]"
|
||||
:style="{ '--avatar-size': `${props.size}px` }"
|
||||
:action="`${apiURL}${props.action}`"
|
||||
:headers="headers"
|
||||
:show-file-list="false"
|
||||
@@ -96,30 +113,65 @@ const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
<style scoped>
|
||||
.avatar-uploader .avatar {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.avatar-uploader .el-upload {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e6e9ee;
|
||||
border: 1px solid hsl(var(--input) / 0.92);
|
||||
border-radius: 50%;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
background: hsl(var(--modal-surface-strong) / 0.92);
|
||||
box-shadow:
|
||||
inset 0 1px 0 hsl(0 0% 100% / 0.72),
|
||||
0 16px 28px -24px hsl(var(--foreground) / 0.24);
|
||||
transition:
|
||||
transform var(--el-transition-duration-fast),
|
||||
border-color var(--el-transition-duration-fast),
|
||||
box-shadow var(--el-transition-duration-fast);
|
||||
}
|
||||
|
||||
.avatar-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
border-color: hsl(var(--primary) / 0.52);
|
||||
transform: translateY(-1px);
|
||||
box-shadow:
|
||||
inset 0 1px 0 hsl(0 0% 100% / 0.76),
|
||||
0 20px 36px -26px hsl(var(--modal-preview-glow) / 0.3);
|
||||
}
|
||||
|
||||
.avatar-uploader--soft-panel .el-upload {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at top,
|
||||
hsl(var(--modal-preview-glow) / 0.14),
|
||||
transparent 60%
|
||||
),
|
||||
linear-gradient(
|
||||
145deg,
|
||||
hsl(var(--modal-preview-surface) / 0.98),
|
||||
hsl(var(--modal-preview-surface-strong) / 0.9)
|
||||
);
|
||||
border-color: hsl(var(--modal-preview-border) / 0.9);
|
||||
}
|
||||
|
||||
.avatar-uploader--rounded .el-upload {
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.el-icon.avatar-uploader-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
font-size: 28px;
|
||||
color: var(--el-text-color-secondary);
|
||||
color: hsl(var(--text-muted));
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user