feat(seller): 素材管理页重构布局并统一主色交互

- 双层卡片嵌套改为单面板,tabs 与操作按钮并入顶部工具栏
- 悬停/选中红色描边统一为 Element Plus 主色
- 复选框改为悬停/选中时显示,选中增加淡蓝遮罩
- 分组行编辑入口悬停显示,添加分组改为置顶虚线按钮
- 分页增加总数显示,空列表显示空状态
This commit is contained in:
pikachu1995@126.com
2026-10-20 13:42:46 +08:00
parent 0c814ec956
commit 7d9bf776db
2 changed files with 410 additions and 345 deletions

View File

@@ -1,193 +1,274 @@
// 弹窗选择器模式:固定内容区高度,避免空分组时布局塌陷 // 商家素材库(OSS 管理)样式:全部规则挂在 .oss-manage 根类下,避免全局污染
.oss-manage-picker { .oss-manage {
.category-tabs-card { // 单面板容器
margin-bottom: 0; .oss-panel {
background: #fff;
:deep(.el-card__body) { border-radius: 4px;
padding-bottom: 0; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
} overflow: hidden;
} }
> .el-row > .el-card { // 顶部工具行:左侧分类 tab + 右侧操作按钮
border: none; .oss-toolbar {
box-shadow: none;
:deep(.el-card__body) {
padding: 0 0 8px;
}
}
.oss-manage-box {
min-height: 520px;
align-items: stretch;
> .el-col {
display: flex;
flex-direction: column;
}
.file-list,
.pic-list {
flex: 1;
min-height: 520px;
}
.pic-list {
display: flex;
flex-direction: column;
}
.img-box {
flex: 1;
min-height: 400px;
}
.page-box {
flex-shrink: 0;
margin-top: auto;
}
}
}
.search {
.oss-operation {
margin-bottom: 2vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; flex-wrap: wrap;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid #ebeef5;
}
button { .oss-tabs {
margin-right: 5px; display: flex;
gap: 8px;
.oss-tab {
padding: 7px 18px;
border-radius: 16px;
font-size: 14px;
color: #606266;
cursor: pointer;
transition:
color 0.2s,
background 0.2s;
&:hover {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
&.active {
color: #fff;
background: var(--el-color-primary);
font-weight: 500;
}
} }
} }
}
.none { .oss-actions {
display: none;
}
.oss-manage-box {
.file-list {
height: 100%;
box-sizing: border-box;
border-right: 1px solid #e5e6eb;
padding: 20px 12px 28px;
}
.pic-list {
height: 100%;
box-sizing: border-box;
padding: 0 0 24px 0;
}
.img-box {
width: 100%;
display: flex; display: flex;
flex-wrap: wrap; align-items: center;
align-items: flex-start; gap: 10px;
justify-content: flex-start; }
align-content: flex-start;
margin-top: 20px; // 内容区:左分组 + 右文件网格
.img-item { .oss-manage-box {
width: 120px; .oss-side {
height: 158px; height: 100%;
box-sizing: border-box; box-sizing: border-box;
margin: 0 13px 8px; border-right: 1px solid #ebeef5;
position: relative; padding: 14px 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; }
.card {
width: 120px;
height: 120px;
border-radius: 8px;
border: 2px solid transparent;
overflow: hidden;
box-sizing: border-box;
position: relative;
img,
.card-media {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-file {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #909399;
}
.card-checkbox {
position: absolute;
top: 8px;
right: 8px;
z-index: 1000;
height: auto;
margin: 0;
:deep(.el-checkbox__label) { .oss-add-group {
display: none; margin-bottom: 10px;
}
.oss-add-group-btn {
width: 100%;
border-style: dashed;
color: #606266;
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
} }
.preview { }
width: 100%; }
height: 26px;
background-color: #ffffff; .oss-main {
color: #666666; height: 100%;
position: absolute; box-sizing: border-box;
left: 0; padding: 2px 16px 16px;
bottom: 0; display: flex;
display: flex; flex-direction: column;
flex-wrap: nowrap; }
align-items: center;
justify-content: center; .oss-grid {
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.12); flex: 1;
> div { display: flex;
flex: 1; flex-wrap: wrap;
align-content: flex-start;
margin-top: 14px;
.oss-grid-item {
width: 120px;
margin: 0 14px 14px 0;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
.oss-card {
width: 120px;
height: 120px;
border-radius: 6px;
border: 2px solid transparent;
overflow: hidden;
box-sizing: border-box;
position: relative;
transition: border-color 0.2s;
img,
.card-media {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.card-file {
width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; background: #f5f7fa;
color: #909399;
}
// 选中遮罩(盖在媒体上方、复选框与操作条下方)
&.is-checked::before {
content: "";
position: absolute;
inset: 0;
background: var(--el-color-primary);
opacity: 0.08;
z-index: 2;
pointer-events: none;
}
.oss-card-check {
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
height: auto;
margin: 0;
opacity: 0;
transition: opacity 0.2s;
:deep(.el-checkbox__label) {
display: none;
}
}
.oss-card-ops {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 30px;
background: rgba(255, 255, 255, 0.96);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
z-index: 3;
> div {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #606266;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
}
&:hover {
border-color: var(--el-color-primary);
.oss-card-check {
opacity: 1;
}
.oss-card-ops {
opacity: 1;
}
}
&.is-checked {
border-color: var(--el-color-primary);
.oss-card-check {
opacity: 1;
}
}
&.oss-card-selectable {
cursor: pointer;
.oss-card-check {
opacity: 1;
}
} }
} }
.oss-name {
width: 120px;
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
color: #606266;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
} }
.card:hover, }
.custom-checkbox-card-checked {
border: 2px solid #ff5c58; .oss-pager {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
}
// 弹窗选择器模式:去掉面板阴影,固定内容区最小高度,避免空分组时布局塌陷
&.oss-manage-picker {
.oss-panel {
border-radius: 0;
box-shadow: none;
}
.oss-toolbar {
padding: 10px 12px;
}
.oss-manage-box {
min-height: 520px;
align-items: stretch;
> .el-col {
display: flex;
flex-direction: column;
} }
.oss-card-selectable {
cursor: pointer; .oss-side,
.oss-main {
flex: 1;
min-height: 520px;
} }
.text {
width: 120px; .oss-grid {
height: 36px; min-height: 400px;
cursor: pointer; }
color: #252931;
font-size: 14px; .oss-pager {
line-height: 36px; margin-top: auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
} }
} }
} }
.page-box {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.pagination-box {
display: flex;
flex-direction: row-reverse;
}
.demo-tree-render .el-tree-node__label {
width: 94%;
}
} }

View File

@@ -1,24 +1,6 @@
<style lang="scss"> <style lang="scss">
@import "./ossManage.scss"; @import "./ossManage.scss";
.group-row {
padding-top: 16px;
margin-top: 8px;
border-top: 1px solid #ededed;
display: flex;
align-items: center;
justify-content: center;
> * {
margin-right: 10px;
}
}
.article-category {
flex: 2;
min-width: 200px;
}
.table { .table {
flex: 11; flex: 11;
} }
@@ -44,33 +26,24 @@
gap: 10px; gap: 10px;
} }
.tab-empty {
padding: 48px 0;
color: #999;
text-align: center;
}
.tabs-card {
margin-bottom: 16px;
}
.oss-group-tree { .oss-group-tree {
padding: 4px 8px 0; flex: 1;
padding: 4px 4px 0;
:deep(.el-tree-node__expand-icon) { :deep(.el-tree-node__expand-icon) {
display: none; display: none;
} }
:deep(.el-tree-node) { :deep(.el-tree-node) {
margin-bottom: 6px; margin-bottom: 4px;
} }
:deep(.el-tree-node__content) { :deep(.el-tree-node__content) {
padding: 0 10px !important; padding: 0 10px !important;
height: 42px; height: 38px;
border-radius: 6px; border-radius: 6px;
font-size: 15px; font-size: 14px;
line-height: 42px; line-height: 38px;
} }
:deep(.el-tree-node__content:hover) { :deep(.el-tree-node__content:hover) {
@@ -78,8 +51,8 @@
} }
:deep(.el-tree-node.is-current > .el-tree-node__content) { :deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #ecf5ff; background: var(--el-color-primary-light-9);
color: #409eff; color: var(--el-color-primary);
font-weight: 500; font-weight: 500;
} }
} }
@@ -90,7 +63,11 @@
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
padding-right: 4px; padding-right: 4px;
font-size: 15px; font-size: 14px;
&:hover .oss-tree-more {
opacity: 1;
}
} }
.oss-tree-more { .oss-tree-more {
@@ -98,6 +75,12 @@
color: #909399; color: #909399;
padding: 0 4px; padding: 0 4px;
font-size: 16px; font-size: 16px;
opacity: 0;
transition: opacity 0.2s;
&:hover {
color: var(--el-color-primary);
}
} }
.file-preview-fallback { .file-preview-fallback {
@@ -124,157 +107,158 @@
</style> </style>
<template> <template>
<div class="search" :class="{ 'oss-manage-picker': isComponent }"> <div class="search oss-manage" :class="{ 'oss-manage-picker': isComponent }">
<el-card class="tabs-card category-tabs-card"> <div class="oss-panel">
<el-tabs v-model="activeCategoryTab" @tab-change="handleCategoryTabChange"> <div class="oss-toolbar">
<el-tab-pane label="图片" name="IMAGE" /> <div class="oss-tabs">
<el-tab-pane label="视频" name="VIDEO" /> <div
<el-tab-pane label="文件" name="FILE" /> class="oss-tab"
</el-tabs> :class="{ active: activeCategoryTab === 'IMAGE' }"
</el-card> @click="handleCategoryTabChange('IMAGE')"
<el-row> >图片</div>
<el-card> <div
<div> class="oss-tab"
<el-row class="oss-manage-box"> :class="{ active: activeCategoryTab === 'VIDEO' }"
<el-col v-if="canShowGroups" :span="isComponent ? 5 : 4"> @click="handleCategoryTabChange('VIDEO')"
<div class="file-list"> >视频</div>
<div class="article-category mr_10"> <div
<el-tree class="oss-tab"
:key="treeRenderKey" :class="{ active: activeCategoryTab === 'FILE' }"
:data="treeData" @click="handleCategoryTabChange('FILE')"
node-key="value" >文件</div>
:props="{ label: 'title', children: 'children' }" </div>
:indent="0" <div class="oss-actions">
highlight-current <el-upload
class="demo-tree-render oss-group-tree" v-if="canUploadImages"
@node-click="handleTreeNodeClick" ref="upRef"
:action="sellerUrl + '/store/common/upload/file'"
:data="uploadData"
:headers="accessToken"
:before-upload="beforeUpload"
:on-error="handleError"
:on-success="handleSuccess"
:show-file-list="false"
multiple
style="display: inline-block"
>
<el-button type="primary">{{ uploadButtonText }}</el-button>
</el-upload>
<el-button v-if="!isComponent && showType == 'list'" @click="removeAll">批量删除</el-button>
</div>
</div>
<el-row class="oss-manage-box">
<el-col v-if="canShowGroups" :span="isComponent ? 5 : 4">
<div class="oss-side">
<div v-if="!isComponent" class="oss-add-group">
<el-button class="oss-add-group-btn" @click="handleClickAddGroup">+ 添加分组</el-button>
</div>
<el-tree
:key="treeRenderKey"
:data="treeData"
node-key="value"
:props="{ label: 'title', children: 'children' }"
:indent="0"
highlight-current
class="demo-tree-render oss-group-tree"
@node-click="handleTreeNodeClick"
>
<template #default="{ data }">
<span v-if="data" class="oss-tree-node">
<span>{{ data.title }}</span>
<el-dropdown
v-if="data.value !== '0' && !isComponent"
trigger="click"
@command="(cmd) => onTreeDropdownCommand(cmd, data)"
> >
<template #default="{ data }"> <span class="oss-tree-more" @click.stop>···</span>
<span v-if="data" class="oss-tree-node"> <template #dropdown>
<span>{{ data.title }}</span> <el-dropdown-menu>
<el-dropdown <el-dropdown-item command="edit">编辑</el-dropdown-item>
v-if="data.value !== '0' && !isComponent" <el-dropdown-item command="delete">删除</el-dropdown-item>
trigger="click" </el-dropdown-menu>
@command="(cmd) => onTreeDropdownCommand(cmd, data)"
>
<span class="oss-tree-more" @click.stop>···</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="edit">编辑</el-dropdown-item>
<el-dropdown-item command="delete">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</template> </template>
</el-tree> </el-dropdown>
<div v-if="!isComponent" class="group-row flex"> </span>
<el-button @click="handleClickAddGroup">添加分组</el-button> </template>
</div> </el-tree>
</div> </div>
</div> </el-col>
</el-col> <el-col :span="canShowGroups ? (isComponent ? 19 : 20) : 24">
<el-col :span="canShowGroups ? (isComponent ? 19 : 20) : 24"> <div class="oss-main">
<div class="pic-list"> <el-checkbox-group v-model="selectedOss" @change="selectOssChange">
<div> <div class="oss-grid">
<div class="oss-operation padding-row" style="display: flex; flex-direction: row-reverse"> <div v-for="(item, index) in data" :key="index" class="oss-grid-item">
<div> <div
<el-upload class="oss-card"
v-if="canUploadImages" :class="{
ref="upRef" 'is-checked': selectedOss.includes(item.id + ',' + item.url),
:action="sellerUrl + '/store/common/upload/file'" 'oss-card-selectable': isComponent && selectImage,
:data="uploadData" }"
:headers="accessToken" @mouseenter="onMouseOver(item, index)"
:before-upload="beforeUpload" @mouseleave="onMouseOut(item, index)"
:on-error="handleError" @click="handleCardClick(item, $event)"
:on-success="handleSuccess" >
:show-file-list="false" <el-checkbox
multiple :value="item.id + ',' + item.url"
style="display: inline-block" class="oss-card-check"
> @click.stop
<el-button>{{ uploadButtonText }}</el-button> />
</el-upload> <img v-if="isImageFile(item)" :src="item.url" alt="" />
<el-button v-if="!isComponent && showType == 'list'" @click="removeAll">批量删除</el-button> <video
v-else-if="isVideoFile(item)"
class="card-media"
:src="item.url"
muted
/>
<div v-else class="card-file">
<el-icon :size="28"><Document /></el-icon>
</div> </div>
</div> <div v-if="item.isShowPreview" class="oss-card-ops">
</div> <div @click.prevent="download(item)">
<div> <el-tooltip content="下载" placement="top">
<el-checkbox-group v-model="selectedOss" @change="selectOssChange"> <el-icon :size="18"><Download /></el-icon>
<div class="img-box"> </el-tooltip>
<div v-for="(item, index) in data" :key="index" class="img-item"> </div>
<div <div @click.prevent="remove(item)">
class="card" <el-tooltip content="删除" placement="top">
:class="{ <el-icon :size="18"><Delete /></el-icon>
'custom-checkbox-card-checked': selectedOss.includes(item.id + ',' + item.url), </el-tooltip>
'oss-card-selectable': isComponent && selectImage, </div>
}" <div
@mouseenter="onMouseOver(item, index)" @click.prevent="previewFile(item)"
@mouseleave="onMouseOut(item, index)" >
@click="handleCardClick(item, $event)" <el-tooltip content="预览" placement="top">
> <el-icon :size="22"><View /></el-icon>
<el-checkbox
:value="item.id + ',' + item.url"
class="card-checkbox"
@click.stop
/>
<img v-if="isImageFile(item)" :src="item.url" alt="" />
<video
v-else-if="isVideoFile(item)"
class="card-media"
:src="item.url"
muted
/>
<div v-else class="card-file">
<el-icon :size="28"><Document /></el-icon>
</div>
<div v-if="item.isShowPreview" class="preview">
<div @click.prevent="download(item)">
<el-tooltip content="下载" placement="top">
<el-icon :size="18"><Download /></el-icon>
</el-tooltip>
</div>
<div @click.prevent="remove(item)">
<el-tooltip content="删除" placement="top">
<el-icon :size="18"><Delete /></el-icon>
</el-tooltip>
</div>
<div
@click.prevent="previewFile(item)"
>
<el-tooltip content="预览" placement="top">
<el-icon :size="22"><View /></el-icon>
</el-tooltip>
</div>
</div>
</div>
<el-tooltip :content="item.name" placement="bottom">
<div class="text">{{ item.name }}</div>
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
</el-checkbox-group> </div>
</div> <el-tooltip :content="item.name" placement="bottom">
<div class="page-box"> <div class="oss-name">{{ item.name }}</div>
<el-pagination </el-tooltip>
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
layout="prev, pager, next, jumper"
size="small"
@current-change="pageChange"
/>
</div>
<div v-if="isComponent && selectImage && !hideSelectFooter" class="oss-select-footer">
<el-button @click="handleSelectCancel">取消</el-button>
<el-button type="primary" @click="handleSelectConfirm">确定</el-button>
</div> </div>
</div> </div>
</el-col> </el-checkbox-group>
</el-row> <el-empty v-if="!loading && !data.length" description="暂无文件" :image-size="100" />
<div class="oss-pager">
</div> <el-pagination
</el-card> v-model:current-page="searchForm.pageNumber"
</el-row> v-model:page-size="searchForm.pageSize"
:total="total"
layout="total, prev, pager, next, jumper"
size="small"
@current-change="pageChange"
/>
</div>
<div v-if="isComponent && selectImage && !hideSelectFooter" class="oss-select-footer">
<el-button @click="handleSelectCancel">取消</el-button>
<el-button type="primary" @click="handleSelectConfirm">确定</el-button>
</div>
</div>
</el-col>
</el-row>
</div>
<el-dialog <el-dialog
v-model="modalVisible" v-model="modalVisible"
:close-on-click-modal="false" :close-on-click-modal="false"