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-manage-picker {
.category-tabs-card {
margin-bottom: 0;
:deep(.el-card__body) {
padding-bottom: 0;
}
// 商家素材库(OSS 管理)样式:全部规则挂在 .oss-manage 根类下,避免全局污染
.oss-manage {
// 单面板容器
.oss-panel {
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
overflow: hidden;
}
> .el-row > .el-card {
border: none;
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;
// 顶部工具行:左侧分类 tab + 右侧操作按钮
.oss-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
flex-wrap: wrap;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid #ebeef5;
}
button {
margin-right: 5px;
.oss-tabs {
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 {
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%;
.oss-actions {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
align-content: flex-start;
margin-top: 20px;
.img-item {
width: 120px;
height: 158px;
align-items: center;
gap: 10px;
}
// 内容区:左分组 + 右文件网格
.oss-manage-box {
.oss-side {
height: 100%;
box-sizing: border-box;
margin: 0 13px 8px;
position: relative;
border-right: 1px solid #ebeef5;
padding: 14px 12px;
display: flex;
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) {
display: none;
}
.oss-add-group {
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;
color: #666666;
position: absolute;
left: 0;
bottom: 0;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.12);
> div {
flex: 1;
}
}
.oss-main {
height: 100%;
box-sizing: border-box;
padding: 2px 16px 16px;
display: flex;
flex-direction: column;
}
.oss-grid {
flex: 1;
display: flex;
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;
align-items: 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;
height: 36px;
cursor: pointer;
color: #252931;
font-size: 14px;
line-height: 36px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
.oss-grid {
min-height: 400px;
}
.oss-pager {
margin-top: auto;
}
}
}
.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">
@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 {
flex: 11;
}
@@ -44,33 +26,24 @@
gap: 10px;
}
.tab-empty {
padding: 48px 0;
color: #999;
text-align: center;
}
.tabs-card {
margin-bottom: 16px;
}
.oss-group-tree {
padding: 4px 8px 0;
flex: 1;
padding: 4px 4px 0;
:deep(.el-tree-node__expand-icon) {
display: none;
}
:deep(.el-tree-node) {
margin-bottom: 6px;
margin-bottom: 4px;
}
:deep(.el-tree-node__content) {
padding: 0 10px !important;
height: 42px;
height: 38px;
border-radius: 6px;
font-size: 15px;
line-height: 42px;
font-size: 14px;
line-height: 38px;
}
:deep(.el-tree-node__content:hover) {
@@ -78,8 +51,8 @@
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #ecf5ff;
color: #409eff;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
}
@@ -90,7 +63,11 @@
justify-content: space-between;
width: 100%;
padding-right: 4px;
font-size: 15px;
font-size: 14px;
&:hover .oss-tree-more {
opacity: 1;
}
}
.oss-tree-more {
@@ -98,6 +75,12 @@
color: #909399;
padding: 0 4px;
font-size: 16px;
opacity: 0;
transition: opacity 0.2s;
&:hover {
color: var(--el-color-primary);
}
}
.file-preview-fallback {
@@ -124,157 +107,158 @@
</style>
<template>
<div class="search" :class="{ 'oss-manage-picker': isComponent }">
<el-card class="tabs-card category-tabs-card">
<el-tabs v-model="activeCategoryTab" @tab-change="handleCategoryTabChange">
<el-tab-pane label="图片" name="IMAGE" />
<el-tab-pane label="视频" name="VIDEO" />
<el-tab-pane label="文件" name="FILE" />
</el-tabs>
</el-card>
<el-row>
<el-card>
<div>
<el-row class="oss-manage-box">
<el-col v-if="canShowGroups" :span="isComponent ? 5 : 4">
<div class="file-list">
<div class="article-category mr_10">
<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"
<div class="search oss-manage" :class="{ 'oss-manage-picker': isComponent }">
<div class="oss-panel">
<div class="oss-toolbar">
<div class="oss-tabs">
<div
class="oss-tab"
:class="{ active: activeCategoryTab === 'IMAGE' }"
@click="handleCategoryTabChange('IMAGE')"
>图片</div>
<div
class="oss-tab"
:class="{ active: activeCategoryTab === 'VIDEO' }"
@click="handleCategoryTabChange('VIDEO')"
>视频</div>
<div
class="oss-tab"
:class="{ active: activeCategoryTab === 'FILE' }"
@click="handleCategoryTabChange('FILE')"
>文件</div>
</div>
<div class="oss-actions">
<el-upload
v-if="canUploadImages"
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 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)"
>
<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>
<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-tree>
<div v-if="!isComponent" class="group-row flex">
<el-button @click="handleClickAddGroup">添加分组</el-button>
</div>
</div>
</div>
</el-col>
<el-col :span="canShowGroups ? (isComponent ? 19 : 20) : 24">
<div class="pic-list">
<div>
<div class="oss-operation padding-row" style="display: flex; flex-direction: row-reverse">
<div>
<el-upload
v-if="canUploadImages"
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>{{ uploadButtonText }}</el-button>
</el-upload>
<el-button v-if="!isComponent && showType == 'list'" @click="removeAll">批量删除</el-button>
</el-dropdown>
</span>
</template>
</el-tree>
</div>
</el-col>
<el-col :span="canShowGroups ? (isComponent ? 19 : 20) : 24">
<div class="oss-main">
<el-checkbox-group v-model="selectedOss" @change="selectOssChange">
<div class="oss-grid">
<div v-for="(item, index) in data" :key="index" class="oss-grid-item">
<div
class="oss-card"
:class="{
'is-checked': selectedOss.includes(item.id + ',' + item.url),
'oss-card-selectable': isComponent && selectImage,
}"
@mouseenter="onMouseOver(item, index)"
@mouseleave="onMouseOut(item, index)"
@click="handleCardClick(item, $event)"
>
<el-checkbox
:value="item.id + ',' + item.url"
class="oss-card-check"
@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>
</div>
<div>
<el-checkbox-group v-model="selectedOss" @change="selectOssChange">
<div class="img-box">
<div v-for="(item, index) in data" :key="index" class="img-item">
<div
class="card"
:class="{
'custom-checkbox-card-checked': selectedOss.includes(item.id + ',' + item.url),
'oss-card-selectable': isComponent && selectImage,
}"
@mouseenter="onMouseOver(item, index)"
@mouseleave="onMouseOut(item, index)"
@click="handleCardClick(item, $event)"
>
<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>
<div v-if="item.isShowPreview" class="oss-card-ops">
<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>
</el-checkbox-group>
</div>
<div class="page-box">
<el-pagination
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>
<el-tooltip :content="item.name" placement="bottom">
<div class="oss-name">{{ item.name }}</div>
</el-tooltip>
</div>
</div>
</el-col>
</el-row>
</div>
</el-card>
</el-row>
</el-checkbox-group>
<el-empty v-if="!loading && !data.length" description="暂无文件" :image-size="100" />
<div class="oss-pager">
<el-pagination
v-model:current-page="searchForm.pageNumber"
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
v-model="modalVisible"
:close-on-click-modal="false"