refactor: extract image form to standalone page and implement tags view store

- Created ImageForm.vue as standalone page for add/edit image functionality
- Removed dialog-based image form from VmImages.vue
- Implemented tagsViewStore for global tab state management
- Added automatic tab closing on form cancel/back
- Fixed data persistence issue when switching between image edits
- Removed quick actions section from ImageForm
- Updated router configuration for new image form route
This commit is contained in:
2025-11-28 14:15:29 +08:00
parent 067e0539ba
commit f7c3be1d30
45 changed files with 8776 additions and 6881 deletions
+156 -40
View File
@@ -3,15 +3,15 @@
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo-container">
<h1 class="title">零零七云计算后台控制面板</h1>
<img src="@/assets/logo.png" alt="Logo" class="logo-img" />
</div>
<el-scrollbar class="sidebar-scrollbar">
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
background-color="#ffffff"
text-color="#333333"
active-text-color="#1890ff"
background-color="transparent"
text-color="#34495e"
active-text-color="#2c3e50"
:unique-opened="true"
router
>
@@ -143,46 +143,39 @@ const handleLogout = () => {
/* 侧边栏样式 */
.sidebar {
width: 240px;
width: 260px;
height: 100%;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-right: 1px solid #e1e8ed;
overflow: hidden;
z-index: 20;
}
.logo-container {
height: 60px;
height: 70px;
display: flex;
align-items: center;
padding: 0 16px;
color: #333;
justify-content: center;
padding: 0 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
overflow: hidden;
border-bottom: 1px solid #e1e8ed;
}
.logo {
width: 32px;
height: 32px;
margin-right: 10px;
}
.title {
font-size: 18px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
color: #1890ff;
.logo-img {
height: 50px;
width: auto;
object-fit: contain;
}
.sidebar-scrollbar {
height: calc(100vh - 60px);
height: calc(100vh - 70px);
}
.sidebar-menu {
border-right: none;
min-height: 100%;
background-color: transparent !important;
padding: 0;
}
/* 主容器样式 */
@@ -197,9 +190,9 @@ const handleLogout = () => {
/* 顶部导航栏样式 */
.navbar {
height: 60px;
padding: 0 15px;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0 20px;
background-color: #ffffff;
border-bottom: 1px solid #e1e8ed;
display: flex;
align-items: center;
justify-content: space-between;
@@ -209,32 +202,35 @@ const handleLogout = () => {
.navbar-left {
display: flex;
align-items: center;
flex: 1;
}
.navbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.navbar-item {
padding: 0 10px;
height: 60px;
display: flex;
align-items: center;
}
.header-btn {
height: 40px;
width: 40px;
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
color: #606266;
color: #34495e;
transition: all 0.2s ease;
border-radius: 0;
}
.header-btn:hover {
background-color: #f5f7fa;
border-radius: 4px;
background-color: #f8f9fa;
color: #2c3e50;
}
.avatar-container {
@@ -243,16 +239,31 @@ const handleLogout = () => {
cursor: pointer;
padding: 0 12px;
height: 60px;
gap: 8px;
transition: all 0.2s ease;
border-radius: 0;
}
.avatar-container:hover {
background-color: rgba(0, 0, 0, 0.025);
background-color: #f8f9fa;
}
.username {
margin: 0 8px;
margin: 0;
font-size: 14px;
color: #606266;
font-weight: 500;
color: #2c3e50;
}
:deep(.avatar-container .el-icon--right) {
color: #7f8c8d;
font-size: 12px;
margin-left: 4px;
transition: color 0.2s ease;
}
.avatar-container:hover :deep(.el-icon--right) {
color: #34495e;
}
/* 内容区域样式 */
@@ -275,13 +286,38 @@ const handleLogout = () => {
opacity: 0;
}
:deep(.el-dropdown-menu) {
border-radius: 0;
border: 1px solid #e1e8ed;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
padding: 4px 0;
}
:deep(.el-dropdown-menu__item) {
display: flex;
align-items: center;
color: #34495e;
transition: all 0.2s ease;
padding: 8px 16px;
}
:deep(.el-dropdown-menu__item i) {
margin-right: 8px;
color: #7f8c8d;
}
:deep(.el-dropdown-menu__item:hover) {
background-color: #f8f9fa;
color: #2c3e50;
}
:deep(.el-dropdown-menu__item:hover i) {
color: #2c3e50;
}
:deep(.el-dropdown-menu__item.is-divided) {
border-top: 1px solid #e1e8ed;
}
/* 侧边栏滚动条样式优化 */
@@ -293,18 +329,98 @@ const handleLogout = () => {
height: 100%;
}
/* 自定义滚动条样式 */
:deep(.sidebar-scrollbar .el-scrollbar__bar) {
opacity: 0.3;
}
:deep(.sidebar-scrollbar .el-scrollbar__thumb) {
background-color: rgba(255, 255, 255, 0.2);
transition: background-color 0.2s;
}
:deep(.sidebar-scrollbar .el-scrollbar__thumb:hover) {
background-color: rgba(255, 255, 255, 0.35);
}
/* Element Plus 菜单项样式优化 */
:deep(.el-menu) {
border-right: none;
}
:deep(.el-sub-menu__title) {
height: 48px;
line-height: 48px;
height: 50px;
line-height: 50px;
margin: 0;
padding: 0 20px;
transition: background-color 0.2s ease;
color: #34495e !important;
}
:deep(.el-sub-menu__title:hover) {
background-color: #f8f9fa !important;
color: #2c3e50 !important;
}
:deep(.el-menu-item) {
height: 48px;
line-height: 48px;
height: 50px;
line-height: 50px;
margin: 0;
padding: 0 20px;
transition: background-color 0.2s ease;
color: #34495e !important;
}
:deep(.el-menu-item:hover) {
background-color: #f8f9fa !important;
color: #2c3e50 !important;
}
:deep(.el-menu-item.is-active) {
background-color: rgba(44, 62, 80, 0.1) !important;
color: #2c3e50 !important;
font-weight: 600;
position: relative;
}
:deep(.el-menu-item.is-active::before) {
content: '';
position: absolute;
left: 0;
top: 0;
width: 3px;
height: 100%;
background-color: #2c3e50;
}
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
color: #2c3e50 !important;
background-color: #f8f9fa !important;
}
:deep(.el-sub-menu .el-menu) {
background-color: #fafbfc !important;
margin: 0;
padding: 0;
}
:deep(.el-sub-menu .el-menu-item) {
margin: 0;
padding-left: 48px !important;
background-color: transparent !important;
}
:deep(.el-sub-menu .el-menu-item.is-active) {
background-color: rgba(44, 62, 80, 0.12) !important;
}
:deep(.el-sub-menu__icon-arrow) {
color: #7f8c8d !important;
transition: transform 0.2s ease;
}
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .el-sub-menu__icon-arrow) {
transform: rotate(180deg);
color: #2c3e50 !important;
}
</style>
+43 -5
View File
@@ -181,34 +181,72 @@ const breadcrumbs = computed(() => {
display: flex;
align-items: center;
font-size: 14px;
height: 100%;
}
.breadcrumb-icon {
margin-right: 6px;
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
transition: color 0.2s ease;
}
:deep(.el-breadcrumb__item) {
display: flex !important;
align-items: center;
height: 100%;
}
:deep(.el-breadcrumb__inner) {
display: flex !important;
align-items: center;
color: #7f8c8d;
font-weight: 400;
font-size: 14px;
transition: all 0.2s ease;
}
:deep(.el-breadcrumb__inner a) {
color: #606266;
font-weight: normal;
transition: color 0.2s ease;
color: #7f8c8d;
font-weight: 400;
transition: all 0.2s ease;
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 0;
}
:deep(.el-breadcrumb__inner a:hover) {
color: #1890ff;
color: #2c3e50;
background-color: #f8f9fa;
}
:deep(.el-breadcrumb__inner.is-link) {
color: #7f8c8d;
}
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
color: #2c3e50;
font-weight: 500;
font-size: 14px;
}
:deep(.el-breadcrumb__item:last-child .breadcrumb-icon) {
color: #2c3e50;
}
:deep(.el-breadcrumb__separator) {
margin: 0 8px;
margin: 0 10px;
color: #bdc3c7;
font-size: 12px;
font-weight: 300;
}
:deep(.el-breadcrumb__item:first-child .el-breadcrumb__inner) {
font-weight: 500;
}
:deep(.el-breadcrumb__item:first-child .breadcrumb-icon) {
color: #2c3e50;
}
</style>
+28 -44
View File
@@ -39,65 +39,49 @@ const hasChildren = computed(() => {
</script>
<style scoped>
.el-menu-item, :deep(.el-sub-menu__title) {
height: 50px;
line-height: 50px;
color: #333333;
}
:deep(.el-sub-menu .el-menu-item) {
height: 50px;
line-height: 50px;
padding-left: 55px !important;
background-color: #fafafa;
}
.el-icon {
margin-right: 10px;
width: 24px;
margin-right: 12px;
width: 20px;
height: 20px;
text-align: center;
color: #666666;
color: #7f8c8d;
transition: color 0.2s ease;
font-size: 18px;
}
/* 激活菜单项特效 */
.el-menu-item.is-active {
position: relative;
background-color: #e6f7ff !important;
color: #1890ff !important;
font-weight: 600;
.el-menu-item .el-icon, :deep(.el-sub-menu__title .el-icon) {
color: #7f8c8d !important;
transition: color 0.2s ease;
}
.el-menu-item.is-active::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: #1890ff;
.el-menu-item:hover .el-icon,
:deep(.el-sub-menu__title:hover .el-icon) {
color: #34495e !important;
}
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
color: #1890ff !important;
font-weight: 600;
/* 激活菜单项图标 */
.el-menu-item.is-active .el-icon {
color: #2c3e50 !important;
}
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) {
background-color: #f5f7fa !important;
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
color: #2c3e50 !important;
}
/* 修复图标颜色 */
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
color: #1890ff !important;
/* 菜单文字样式 */
.el-menu-item span, :deep(.el-sub-menu__title span) {
font-size: 14px;
letter-spacing: 0.2px;
}
/* 修复箭头颜色 */
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) {
color: #1890ff !important;
/* 子菜单项样式优化 */
:deep(.el-sub-menu .el-menu-item) {
font-size: 13px;
padding-left: 48px !important;
}
/* 子菜单样式 */
:deep(.el-menu--inline) {
background-color: #fafafa;
:deep(.el-sub-menu .el-menu-item .el-icon) {
font-size: 16px;
margin-right: 10px;
}
</style>
+104 -106
View File
@@ -59,14 +59,16 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import { useTagsViewStore } from '@/store/tagsViewStore'
const router = useRouter()
const route = useRoute()
const tagsViewStore = useTagsViewStore()
// 访问过的标签 (从 store 获取)
const visitedViews = computed(() => tagsViewStore.visitedViews)
const affixTags = computed(() => tagsViewStore.affixTags)
// 固定标签
const affixTags = ref([])
// 访问过的标签
const visitedViews = ref([])
// 右键菜单
const visible = ref(false)
const top = ref(0)
@@ -77,101 +79,67 @@ const selectedTag = ref({})
const initTags = () => {
// 如果当前路由不在访问过的标签中,添加它
if (route.name) {
addVisitedView(route)
tagsViewStore.addVisitedView(route)
}
// 添加固定标签(仪表盘)
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
if (dashboardRoute) {
affixTags.value.push(dashboardRoute)
addVisitedView(dashboardRoute)
// 注意:这里我们直接修改 store 的 affixTags,或者 store 应该提供一个 action
// 简单起见,我们假设 store 的 affixTags 是可以直接修改的 ref,或者我们在 store 中添加初始化逻辑
// 但为了保持一致性,我们这里只处理 visitedViews 的添加
if (!tagsViewStore.affixTags.some(tag => tag.path === dashboardRoute.path)) {
tagsViewStore.affixTags.push(dashboardRoute)
}
tagsViewStore.addVisitedView(dashboardRoute)
}
}
// 添加访问过的标签
const addVisitedView = (view) => {
if (visitedViews.value.some(v => v.path === view.path)) return
// 过滤404和登录页
if (view.name === 'NotFound' || view.name === 'Login') return
visitedViews.value.push(
Object.assign({}, view, {
title: view.meta.title || 'unknown'
})
)
}
// 刷新选中的标签
const refreshSelectedTag = (view) => {
// 路由刷新的原理是先获取当前路由的全部信息,然后将路由重定向到一个空白页,
// 然后立即再将路由重定向回原路由,实现刷新效果
const { fullPath } = view
router.replace('/redirect' + fullPath)
}
// 关闭选中的标签
const closeSelectedTag = (view) => {
// 从访问过的标签中移除
const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index > -1) {
visitedViews.value.splice(index, 1)
}
// 如果关闭的是当前标签,则跳转到下一个标签
if (isActive(view)) {
toLastView(visitedViews.value, view)
}
tagsViewStore.delVisitedView(view).then((visitedViews) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
// 关闭其他标签
const closeOthersTags = () => {
// 保留固定标签和当前选中的标签
visitedViews.value = visitedViews.value.filter(v => {
return isAffix(v) || v.path === selectedTag.value.path
router.push(selectedTag.value)
tagsViewStore.delOthersViews(selectedTag.value).then(() => {
// moveToCurrentTag() // 如果有滚动逻辑
})
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
}
// 关闭左侧标签
const closeLeftTags = () => {
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
if (selectedIndex === -1) return
// 保留固定标签和右侧标签
visitedViews.value = visitedViews.value.filter((v, i) => {
return isAffix(v) || i >= selectedIndex
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
if (!visitedViews.find(i => i.path === route.path)) {
toLastView(visitedViews)
}
})
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
}
// 关闭右侧标签
const closeRightTags = () => {
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path)
if (selectedIndex === -1) return
// 保留固定标签和左侧标签
visitedViews.value = visitedViews.value.filter((v, i) => {
return isAffix(v) || i <= selectedIndex
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
if (!visitedViews.find(i => i.path === route.path)) {
toLastView(visitedViews)
}
})
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
}
// 关闭所有标签
const closeAllTags = () => {
// 仅保留固定标签
visitedViews.value = visitedViews.value.filter(v => isAffix(v))
// 跳转到第一个标签或首页
toLastView(visitedViews.value)
tagsViewStore.delAllViews().then((visitedViews) => {
toLastView(visitedViews)
})
}
// 跳转到最后一个标签或首页
@@ -182,7 +150,6 @@ const toLastView = (visitedViews, view) => {
} else {
// 如果没有标签,则跳转到首页
if (view && view.name === 'Dashboard') {
// 如果当前是首页,则刷新页面
router.push('/redirect' + '/dashboard')
} else {
router.push('/')
@@ -197,7 +164,7 @@ const isActive = (tag) => {
// 判断是否是固定标签
const isAffix = (tag) => {
return affixTags.value.some(t => t.path === tag.path)
return tag.meta && tag.meta.affix
}
// 打开右键菜单
@@ -222,7 +189,7 @@ const closeMenu = () => {
// 监听路由变化,添加标签
watch(route, (newRoute) => {
if (newRoute.name) {
addVisitedView(newRoute)
tagsViewStore.addVisitedView(newRoute)
}
})
@@ -245,9 +212,8 @@ onBeforeUnmount(() => {
.tags-view-container {
height: 40px;
width: 100%;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
background-color: #ffffff;
border-bottom: 1px solid #e1e8ed;
z-index: 10;
}
@@ -256,7 +222,7 @@ onBeforeUnmount(() => {
width: 100%;
display: flex;
align-items: center;
padding: 0 16px;
padding: 0 12px;
overflow-x: auto;
white-space: nowrap;
position: relative;
@@ -270,41 +236,56 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
height: 100%;
gap: 4px;
}
.tag, .active-tag {
height: 28px;
height: 32px;
display: inline-flex;
align-items: center;
padding: 0 10px;
margin-right: 5px;
border-radius: 2px;
font-size: 12px;
color: #333333;
background-color: #f4f4f5;
padding: 0 12px;
margin-right: 0;
border-radius: 0;
font-size: 13px;
text-decoration: none;
position: relative;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid #e8e8e8;
transition: all 0.2s ease;
border: 1px solid transparent;
border-bottom: none;
}
.tag {
color: #7f8c8d;
background-color: #f8f9fa;
}
.tag:hover {
color: #1890ff;
background-color: #e6f7ff;
border-color: #1890ff;
color: #34495e;
background-color: #e8ecf0;
}
.active-tag {
color: #1890ff;
background-color: #e6f7ff;
border-color: #1890ff;
font-weight: 600;
color: #2c3e50;
background-color: #ffffff;
border: 1px solid #e1e8ed;
border-bottom: 2px solid #2c3e50;
font-weight: 500;
}
.tag-icon {
margin-right: 4px;
margin-right: 6px;
width: 14px;
height: 14px;
color: #95a5a6;
transition: color 0.2s ease;
}
.tag:hover .tag-icon {
color: #34495e;
}
.active-tag .tag-icon {
color: #2c3e50;
}
.tag-title {
@@ -315,36 +296,46 @@ onBeforeUnmount(() => {
}
.tag-close {
margin-left: 5px;
width: 14px;
height: 14px;
border-radius: 50%;
transition: all 0.3s;
margin-left: 8px;
width: 16px;
height: 16px;
border-radius: 0;
transition: all 0.2s ease;
color: #95a5a6;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
}
.tag-close:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
.tag:hover .tag-close {
color: #666;
background-color: rgba(0, 0, 0, 0.1);
color: #7f8c8d;
}
.active-tag .tag-close {
color: #1890ff;
color: #7f8c8d;
}
.active-tag:hover .tag-close {
background-color: rgba(24, 144, 255, 0.1);
.active-tag .tag-close:hover {
color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
.contextmenu {
position: fixed;
z-index: 100;
background-color: #fff;
background-color: #ffffff;
list-style-type: none;
padding: 6px 0;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 4px 0;
border-radius: 0;
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
font-size: 12px;
border: 1px solid #ebeef5;
border: 1px solid #e1e8ed;
}
.contextmenu li {
@@ -352,15 +343,22 @@ onBeforeUnmount(() => {
cursor: pointer;
display: flex;
align-items: center;
color: #34495e;
transition: all 0.2s ease;
}
.contextmenu li:hover {
background-color: #f5f7fa;
color: #1890ff;
background-color: #f8f9fa;
color: #2c3e50;
}
.contextmenu li .el-icon {
margin-right: 8px;
font-size: 14px;
color: #7f8c8d;
}
.contextmenu li:hover .el-icon {
color: #2c3e50;
}
</style>