Files
schoolNews/schoolNewsWeb/src/views/admin/manage/content/BannerManagementView.vue

1415 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AdminLayout title="内容管理" subtitle="管理网站横幅、栏目、标签等内容信息">
<div class="banner-management">
<!-- 操作栏 -->
<div class="toolbar">
<h3 class="section-title">轮播Banner</h3>
<button class="btn-add" @click="handleCreate">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 3.33333V12.6667M3.33333 8H12.6667" stroke="currentColor" stroke-width="1.33333"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
添加Banner
</button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>加载中...</p>
</div>
<!-- Banner 卡片列表 -->
<div v-else class="banner-list">
<div v-for="banner in banners" :key="banner.bannerID" class="banner-card">
<!-- Banner 图片容器 -->
<div class="banner-image-wrapper">
<img :src="getBannerImageUrl(banner)" :alt="banner.title" class="banner-image" @error="handleImageError" />
</div>
<!-- Banner 信息区域 -->
<div class="banner-content">
<!-- 上部标题和状态 -->
<div class="banner-header">
<div class="banner-text">
<h4 class="banner-title">{{ banner.title || '未命名横幅' }}</h4>
<p class="banner-link">链接: {{ banner.linkUrl || '无' }}</p>
</div>
<div class="banner-status" :class="{ active: banner.status === 1 }">
{{ banner.status === 1 ? '启用' : '停用' }}
</div>
</div>
<!-- 下部排序和操作按钮 -->
<div class="banner-footer">
<div class="banner-order">排序: {{ banner.orderNum || 0 }}</div>
<div class="banner-actions">
<button class="btn-icon-action" @click="handleEdit(banner)" title="编辑">
<img src="@/assets/imgs/edit.svg" alt="编辑" />
</button>
<button class="btn-icon-action btn-delete" @click="handleDelete(banner)" title="删除">
<img src="@/assets/imgs/trashbin.svg" alt="删除" />
</button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="banners.length === 0" class="empty-state">
<div class="empty-icon">📋</div>
<p class="empty-text">暂无横幅数据</p>
<button class="btn-primary" @click="handleCreate">
<span class="btn-icon">+</span>
创建第一个横幅
</button>
</div>
</div>
<!-- 分页组件 -->
<div v-if="!loading && pageParam.totalElements && pageParam.totalElements > 0" class="pagination-container">
<el-pagination v-model:current-page="pageParam.pageNumber" v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]" :total="pageParam.totalElements"
layout="total, sizes, prev, pager, next, jumper" @size-change="handlePageSizeChange"
@current-change="handlePageChange" />
</div>
<!-- 创建/编辑弹窗 -->
<div v-if="dialogVisible" class="dialog-overlay" @click="handleCloseDialog">
<div class="dialog-container" @click.stop>
<div class="dialog-header">
<h3 class="dialog-title">{{ isEdit ? '编辑横幅' : '创建横幅' }}</h3>
<button class="dialog-close" @click="handleCloseDialog">×</button>
</div>
<div class="dialog-body">
<div class="form-group">
<label class="form-label">
<span class="required">*</span>
横幅标题
</label>
<input v-model="currentBanner.title" type="text" class="form-input" placeholder="请输入横幅标题" />
</div>
<div class="form-group">
<label class="form-label">
<span class="required">*</span>
横幅图片
</label>
<FileUpload :as-dialog="false" list-type="cover" v-model:cover-url="currentBanner.imageUrl"
accept="image/*" :max-size="10" tip="支持 jpg、png、gif 格式建议尺寸1920x600" module="banner" />
</div>
<div class="form-group">
<label class="form-label">
<span class="required">*</span>
链接类型
</label>
<div class="radio-group">
<label class="radio-item">
<input v-model="currentBanner.linkType" type="radio" :value="1" />
<span class="radio-label">资源</span>
</label>
<label class="radio-item">
<input v-model="currentBanner.linkType" type="radio" :value="2" />
<span class="radio-label">课程</span>
</label>
<label class="radio-item">
<input v-model="currentBanner.linkType" type="radio" :value="3" />
<span class="radio-label">外部链接</span>
</label>
</div>
</div>
<div class="form-group" v-if="currentBanner.linkType === 1 || currentBanner.linkType === 2">
<label class="form-label">
<span class="required">*</span>
{{ currentBanner.linkType === 1 ? '选择资源' : '选择课程' }}
</label>
<div class="select-container">
<div class="form-select" @click="toggleDropdown" :class="{ 'active': dropdownVisible }">
<span class="select-value">
{{ getSelectedItemLabel() || `请选择${currentBanner.linkType === 1 ? '资源' : '课程'}` }}
</span>
<svg class="select-arrow" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</div>
<!-- 下拉列表 -->
<div v-if="dropdownVisible" class="dropdown-list" @click.stop>
<!-- 搜索框 -->
<div class="dropdown-search">
<input type="text" v-model="searchKeyword" class="search-input"
:placeholder="`搜索${currentBanner.linkType === 1 ? '资源' : '课程'}名称`" @input="handleSearchChange" />
</div>
<!-- 列表内容 -->
<div class="dropdown-content" @scroll="handleScroll" ref="dropdownContent">
<div v-for="item in selectOptions" :key="item.id" class="dropdown-item"
:class="{ 'selected': currentBanner.linkID === item.id }" @click="handleSelectItem(item)">
<span class="item-title">{{ item.title }}</span>
<span class="item-id">ID: {{ item.id }}</span>
</div>
<!-- 加载状态 -->
<div v-if="loadingMore" class="dropdown-loading">
<div class="loading-spinner-small"></div>
<span>加载中...</span>
</div>
<!-- 没有更多数据 -->
<div v-if="!loadingMore && noMoreData && selectOptions.length > 0" class="dropdown-no-more">
没有更多数据
</div>
<!-- 空状态 -->
<div v-if="selectOptions.length === 0 && !loadingMore" class="dropdown-empty">
暂无数据
</div>
</div>
</div>
</div>
</div>
<div class="form-group" v-if="currentBanner.linkType === 3">
<label class="form-label">
<span class="required">*</span>
外部链接
</label>
<input v-model="currentBanner.linkUrl" type="text" class="form-input" placeholder="请输入外部链接地址" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">排序号</label>
<input v-model.number="currentBanner.orderNum" type="number" class="form-input" placeholder="数字越小越靠前" />
</div>
<div class="form-group">
<label class="form-label">状态</label>
<div class="switch-group">
<label class="switch-item">
<input v-model="currentBanner.status" type="radio" :value="1" />
<span class="switch-label">启用</span>
</label>
<label class="switch-item">
<input v-model="currentBanner.status" type="radio" :value="0" />
<span class="switch-label">禁用</span>
</label>
</div>
</div>
</div>
</div>
<div class="dialog-footer">
<button class="btn-secondary" @click="handleCloseDialog">
取消
</button>
<button class="btn-primary" @click="handleSubmit" :disabled="submitting">
{{ submitting ? '提交中...' : (isEdit ? '更新' : '创建') }}
</button>
</div>
</div>
</div>
</div>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick } from 'vue';
import { ElMessage, ElMessageBox, ElPagination } from 'element-plus';
import type { Banner, PageParam } from '@/types';
import { bannerApi } from '@/apis/resource/banner';
import { resourceApi } from '@/apis/resource/resource';
import { courseApi } from '@/apis/study/course';
import { FileUpload } from '@/components';
import { FILE_DOWNLOAD_URL } from '@/config';
import { AdminLayout } from '@/views/admin';
// 数据状态
const loading = ref(false);
const submitting = ref(false);
const banners = ref<Banner[]>([]);
// 下拉选择器相关状态
const dropdownVisible = ref(false);
const selectOptions = ref<any[]>([]);
const searchKeyword = ref('');
const loadingMore = ref(false);
const noMoreData = ref(false);
const dropdownContent = ref<HTMLElement | null>(null);
// 选择器分页参数
const selectPageParam = ref({
pageNumber: 1,
pageSize: 20,
totalPages: 0,
totalElements: 0,
});
// 防抖timer
let searchTimer: ReturnType<typeof setTimeout> | null = null;
// 分页状态
const pageParam = ref<PageParam>({
pageNumber: 1,
pageSize: 10,
totalPages: 0,
totalElements: 0,
});
// 对话框状态
const dialogVisible = ref(false);
const isEdit = ref(false);
const currentBanner = ref<Banner>({
linkType: 1,
status: 1,
orderNum: 0,
});
// 加载横幅列表
async function loadBanners() {
try {
loading.value = true;
const result = await bannerApi.getBannerPage(pageParam.value);
banners.value = result.pageDomain?.dataList || [];
// 更新分页信息
if (result.pageParam) {
pageParam.value = {
...pageParam.value,
...result.pageParam,
};
}
} catch (error) {
console.error('加载横幅列表失败:', error);
ElMessage.error('加载横幅列表失败');
} finally {
loading.value = false;
}
}
// 切换页码
function handlePageChange(page: number) {
pageParam.value.pageNumber = page;
loadBanners();
}
// 切换每页条数
function handlePageSizeChange(size: number) {
pageParam.value.pageSize = size;
pageParam.value.pageNumber = 1;
loadBanners();
}
// 创建横幅
function handleCreate() {
isEdit.value = false;
currentBanner.value = {
linkType: 1,
status: 1,
orderNum: 0,
};
dialogVisible.value = true;
}
// 编辑横幅
async function handleEdit(banner: Banner) {
isEdit.value = true;
currentBanner.value = { ...banner };
dialogVisible.value = true;
// 如果有linkID预加载该项的信息以便显示标题
if (banner.linkID && (banner.linkType === 1 || banner.linkType === 2)) {
await loadSelectedItem(banner.linkID, banner.linkType);
}
}
// 加载已选择项的信息
async function loadSelectedItem(linkID: string, linkType: number) {
try {
if (linkType === 1) {
// 加载资源详情
const result = await resourceApi.getResourceById(linkID);
if (result.code === 200 && result.data) {
const item = result.data;
const option = {
id: item.resource.resourceID,
title: item.resource.title || '未命名'
};
// 如果不在列表中,添加到开头
if (!selectOptions.value.find(opt => opt.id === option.id)) {
selectOptions.value = [option, ...selectOptions.value];
}
}
} else if (linkType === 2) {
// 加载课程详情
const result = await courseApi.getCourseById(linkID);
if (result.code === 200 && result.data) {
const item = result.data;
const option = {
id: item.courseID,
title: item.name
};
// 如果不在列表中,添加到开头
if (!selectOptions.value.find(opt => opt.id === option.id)) {
selectOptions.value = [option, ...selectOptions.value];
}
}
}
} catch (error) {
console.error('加载已选择项失败:', error);
// 失败时不影响编辑功能,只是可能看不到标题
}
}
// 关闭对话框
function handleCloseDialog() {
dialogVisible.value = false;
currentBanner.value = {
linkType: 1,
status: 1,
orderNum: 0,
};
}
// 提交表单
async function handleSubmit() {
// 验证
if (!currentBanner.value.title) {
ElMessage.warning('请输入横幅标题');
return;
}
if (!currentBanner.value.imageUrl) {
ElMessage.warning('请输入图片地址');
return;
}
// 校验链接内容:资源、课程或外部链接必须有一个有值
if (currentBanner.value.linkType === 1 || currentBanner.value.linkType === 2) {
// 选择资源或课程时,必须选择具体的资源/课程
if (!currentBanner.value.linkID) {
ElMessage.warning(`请选择${currentBanner.value.linkType === 1 ? '资源' : '课程'}`);
return;
}
} else if (currentBanner.value.linkType === 3) {
// 选择外部链接时,必须输入链接地址
if (!currentBanner.value.linkUrl || currentBanner.value.linkUrl.trim() === '') {
ElMessage.warning('请输入外部链接地址');
return;
}
}
try {
submitting.value = true;
if (isEdit.value) {
await bannerApi.updateBanner(currentBanner.value);
ElMessage.success('更新成功');
} else {
await bannerApi.createBanner(currentBanner.value);
ElMessage.success('创建成功');
}
handleCloseDialog();
await loadBanners();
} catch (error) {
console.error('保存横幅失败:', error);
ElMessage.error('保存横幅失败');
} finally {
submitting.value = false;
}
}
// 切换状态
async function handleToggleStatus(banner: Banner) {
try {
const newStatus = banner.status === 1 ? 0 : 1;
await bannerApi.updateBannerStatus(banner.id!, newStatus);
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用');
await loadBanners();
} catch (error) {
console.error('更新状态失败:', error);
ElMessage.error('更新状态失败');
}
}
// 删除横幅
async function handleDelete(banner: Banner) {
try {
await ElMessageBox.confirm(
`确定要删除横幅"${banner.title}"吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
await bannerApi.deleteBannerById(banner.bannerID!);
ElMessage.success('删除成功');
await loadBanners();
} catch (error) {
if (error !== 'cancel') {
console.error('删除横幅失败:', error);
ElMessage.error('删除横幅失败');
}
}
}
// 获取链接类型文本
function getLinkTypeText(linkType?: number): string {
const types: Record<number, string> = {
1: '资源',
2: '课程',
3: '外部链接',
};
return types[linkType || 1] || '未知';
}
// 格式化日期
function formatDate(dateStr?: string): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
// 获取Banner图片URL
function getBannerImageUrl(banner: Banner): string {
if (!banner.imageUrl) {
return '/img/article-default.png';
}
// 如果已经是完整URLhttp或https开头直接返回
if (banner.imageUrl.startsWith('http://') || banner.imageUrl.startsWith('https://')) {
return banner.imageUrl;
}
// 否则拼接文件下载URL
return FILE_DOWNLOAD_URL + banner.imageUrl;
}
// 图片加载错误处理
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
img.src = '/img/article-default.png';
}
// ==================== 下拉选择器相关函数 ====================
// 加载选择器数据(资源或课程)
async function loadSelectOptions(reset = false) {
if (loadingMore.value) return;
if (noMoreData.value && !reset) return;
if (reset) {
selectPageParam.value.pageNumber = 1;
selectOptions.value = [];
noMoreData.value = false;
}
try {
loadingMore.value = true;
let result;
if (currentBanner.value.linkType === 1) {
// 加载资源列表
result = await resourceApi.getResourcePage(
selectPageParam.value,
searchKeyword.value ? { title: searchKeyword.value } : undefined
);
} else if (currentBanner.value.linkType === 2) {
// 加载课程列表
result = await courseApi.getCoursePage(
selectPageParam.value,
searchKeyword.value ? { title: searchKeyword.value } as any : undefined
);
}
if (result && result.pageDomain?.dataList) {
const newOptions = result.pageDomain.dataList.map((item: any) => {
// 根据linkType选择正确的ID字段
let itemId = '';
let itemTitle = '';
if (currentBanner.value.linkType === 1) {
// 资源使用resourceID
itemId = item.resourceID;
itemTitle = item.title;
} else if (currentBanner.value.linkType === 2) {
// 课程使用courseID
itemId = item.courseID;
itemTitle = item.name;
}
return {
id: itemId,
title: itemTitle
};
});
// 去重合并数据
const existingIds = new Set(selectOptions.value.map(opt => opt.id));
const uniqueNewOptions = newOptions.filter((opt: any) => !existingIds.has(opt.id));
selectOptions.value = [...selectOptions.value, ...uniqueNewOptions];
// 更新分页信息
if (result.pageParam) {
selectPageParam.value = {
...selectPageParam.value,
...result.pageParam
};
}
// 检查是否还有更多数据
noMoreData.value = selectPageParam.value.pageNumber >= selectPageParam.value.totalPages;
}
} catch (error) {
console.error('加载选择器数据失败:', error);
ElMessage.error('加载数据失败');
} finally {
loadingMore.value = false;
}
}
// 切换下拉框显示
function toggleDropdown() {
dropdownVisible.value = !dropdownVisible.value;
if (dropdownVisible.value) {
// 打开时加载数据(不重置已选项)
searchKeyword.value = '';
// 总是加载第一页数据(保留已选项)
const hasPreloadedItem = selectOptions.value.length === 1 &&
selectOptions.value[0].id === currentBanner.value.linkID;
if (hasPreloadedItem) {
// 如果只有预加载的一项,重置并加载完整列表(会保留预加载项因为去重逻辑)
selectPageParam.value.pageNumber = 1;
noMoreData.value = false;
loadSelectOptions(false); // false = 追加模式,不会清空已有的预加载项
} else if (selectOptions.value.length === 0) {
// 列表为空时,重置并加载
loadSelectOptions(true);
}
// 点击外部关闭下拉框
nextTick(() => {
document.addEventListener('click', closeDropdown);
});
} else {
document.removeEventListener('click', closeDropdown);
}
}
// 关闭下拉框
function closeDropdown() {
dropdownVisible.value = false;
document.removeEventListener('click', closeDropdown);
}
// 处理搜索输入
function handleSearchChange() {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
loadSelectOptions(true);
}, 500); // 500ms防抖
}
// 处理滚动加载
function handleScroll(event: Event) {
const target = event.target as HTMLElement;
const scrollTop = target.scrollTop;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;
// 滚动到距离底部50px时加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
if (!loadingMore.value && !noMoreData.value) {
selectPageParam.value.pageNumber += 1;
loadSelectOptions(false);
}
}
}
// 选择项目
function handleSelectItem(item: any) {
currentBanner.value.linkID = item.id;
console.log('选择项目:', { linkType: currentBanner.value.linkType, linkID: item.id, title: item.title });
dropdownVisible.value = false;
document.removeEventListener('click', closeDropdown);
}
// 获取已选择项的标签
function getSelectedItemLabel(): string {
if (!currentBanner.value.linkID) return '';
const selected = selectOptions.value.find(opt => opt.id === currentBanner.value.linkID);
const label = selected ? selected.title : '';
return label;
}
// 监听linkType变化重新加载数据
watch(() => currentBanner.value.linkType, (newType, oldType) => {
if (newType !== oldType && (newType === 1 || newType === 2)) {
// 切换类型时清空已选择的ID和选项列表
selectOptions.value = [];
noMoreData.value = false;
selectPageParam.value.pageNumber = 1;
// 如果下拉框是打开的,重新加载数据
if (dropdownVisible.value) {
loadSelectOptions(true);
}
}
});
// 组件挂载时加载数据
onMounted(() => {
loadBanners();
});
</script>
<style scoped lang="scss">
.banner-management {
width: 100%;
height: 100%;
background: #FFFFFF;
border-radius: 14px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 48px;
box-sizing: border-box;
overflow: hidden;
}
// 工具栏
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
.section-title {
margin: 0;
font-family: Inter, sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: -0.02em;
color: #101828;
}
}
// 添加按钮样式
.btn-add {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 36px;
padding: 0 16px;
background: #E7000B;
color: #FFFFFF;
border: none;
border-radius: 8px;
font-family: Inter, sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 20px;
letter-spacing: -0.01em;
cursor: pointer;
transition: all 0.2s ease;
svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
&:hover {
background: #C5000A;
}
&:active {
background: #A00008;
}
}
// 主要按钮(对话框用)
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 20px;
background: #E7000B;
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #C5000A;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-icon {
font-size: 18px;
font-weight: 600;
}
}
.btn-secondary {
padding: 10px 20px;
background-color: white;
color: #606266;
border: 1px solid #dcdfe6;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background-color: #f5f7fa;
border-color: #c0c4cc;
}
}
// 加载状态
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e4e7ed;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
p {
margin-top: 16px;
font-size: 14px;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// Banner 列表
.banner-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
min-height: 0;
// 美化滚动条
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
}
// Banner 卡片
.banner-card {
display: flex;
flex-direction: row;
gap: 16px;
background: #F9FAFB;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 14px;
padding: 16px;
height: 146px;
box-sizing: border-box;
transition: all 0.2s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
}
// 图片容器
.banner-image-wrapper {
flex-shrink: 0;
width: 192px;
height: 112px;
background: #E5E7EB;
border-radius: 10px;
overflow: hidden;
.banner-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
// 内容区域
.banner-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
// 上部区域(标题和状态)
.banner-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.banner-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
.banner-title {
margin: 0;
font-family: Inter, sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: -0.02em;
color: #101828;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.banner-link {
margin: 0;
font-family: Inter, sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.01em;
color: #4A5565;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
// 状态徽章
.banner-status {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 2px 8px;
background: #F3F4F6;
border: 1px solid transparent;
border-radius: 8px;
font-family: Inter, sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: #364153;
height: 22px;
&.active {
background: #DCFCE7;
color: #008236;
}
}
// 下部区域(排序和操作)
.banner-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 16px;
.banner-order {
font-family: Inter, sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.01em;
color: #4A5565;
}
}
// 操作按钮容器
.banner-actions {
display: flex;
flex-direction: row;
gap: 8px;
}
// 图标按钮
.btn-icon-action {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: #0A0A0A;
cursor: pointer;
transition: all 0.2s ease;
svg {
width: 16px;
height: 16px;
}
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.btn-delete {
color: #E7000B;
&:hover {
background: rgba(231, 0, 11, 0.1);
}
}
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
background: white;
border-radius: 12px;
grid-column: 1 / -1;
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
margin: 0 0 24px 0;
color: #909399;
font-size: 14px;
}
}
// 对话框
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.dialog-container {
width: 90%;
max-width: 600px;
max-height: 90vh;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e4e7ed;
.dialog-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.dialog-close {
width: 32px;
height: 32px;
background: none;
border: none;
font-size: 24px;
color: #909399;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 6px;
&:hover {
background-color: #f5f7fa;
color: #303133;
}
}
}
.dialog-body {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid #e4e7ed;
}
// 表单样式
.form-group {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: #606266;
.required {
color: #f56c6c;
margin-right: 4px;
}
}
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
color: #606266;
transition: all 0.3s ease;
box-sizing: border-box;
&:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
&::placeholder {
color: #c0c4cc;
}
}
.image-preview {
margin-top: 12px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e4e7ed;
img {
width: 100%;
height: auto;
max-height: 200px;
object-fit: cover;
}
}
.radio-group,
.switch-group {
display: flex;
gap: 16px;
}
.radio-item,
.switch-item {
display: flex;
align-items: center;
cursor: pointer;
input[type="radio"] {
margin-right: 6px;
cursor: pointer;
}
.radio-label,
.switch-label {
font-size: 14px;
color: #606266;
cursor: pointer;
}
}
// ==================== 下拉选择器样式 ====================
.select-container {
position: relative;
width: 100%;
}
.form-select {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 12px;
background-color: white;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
color: #606266;
cursor: pointer;
transition: all 0.3s ease;
box-sizing: border-box;
&:hover {
border-color: #c0c4cc;
}
&.active {
border-color: #E7000B;
}
.select-value {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-arrow {
margin-left: 8px;
flex-shrink: 0;
transition: transform 0.3s ease;
color: #909399;
}
&.active .select-arrow {
transform: rotate(180deg);
}
}
.dropdown-list {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
max-height: 320px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dropdown-search {
padding: 8px;
border-bottom: 1px solid #e4e7ed;
flex-shrink: 0;
.search-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
outline: none;
transition: border-color 0.3s ease;
box-sizing: border-box;
&:focus {
border-color: #E7000B;
}
&::placeholder {
color: #c0c4cc;
}
}
}
.dropdown-content {
flex: 1;
overflow-y: auto;
max-height: 240px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.dropdown-item {
display: flex;
flex-direction: column;
padding: 10px 12px;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid #f5f7fa;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f5f7fa;
}
&.selected {
background-color: rgba(231, 0, 11, 0.05);
color: #E7000B;
.item-id {
color: #E7000B;
}
}
.item-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-id {
font-size: 12px;
color: #909399;
}
}
.dropdown-loading,
.dropdown-no-more,
.dropdown-empty {
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
color: #909399;
font-size: 13px;
gap: 8px;
}
.loading-spinner-small {
width: 16px;
height: 16px;
border: 2px solid #e4e7ed;
border-top-color: #E7000B;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.dropdown-empty {
padding: 20px;
color: #c0c4cc;
}
</style>