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
+284 -45
View File
@@ -1,38 +1,68 @@
<template>
<div class="user-list-container">
<!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="关键字">
<el-input v-model="queryParams.key" placeholder="请输入用户名/邮箱" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="success" @click="fetchUserList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增用户
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
<!-- 搜索和用户列表 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="关键字">
<el-input v-model="queryParams.key" placeholder="请输入用户名/邮箱" clearable style="width: 200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="success" @click="fetchUserList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增用户
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div>
</el-card>
<!-- 用户列表 -->
<el-card class="table-container" shadow="never">
<!-- 用户列表 -->
<div class="table-section">
<!-- 骨架屏 -->
<div v-if="loading" class="skeleton-container">
<div v-for="i in 5" :key="i" class="skeleton-row">
<div class="skeleton-cell skeleton-checkbox"></div>
<div class="skeleton-cell skeleton-id"></div>
<div class="skeleton-cell skeleton-user">
<div class="skeleton-avatar"></div>
<div class="skeleton-text-group">
<div class="skeleton-text skeleton-text-primary"></div>
<div class="skeleton-text skeleton-text-secondary"></div>
</div>
</div>
<div class="skeleton-cell skeleton-avatar"></div>
<div class="skeleton-cell skeleton-phone"></div>
<div class="skeleton-cell skeleton-realname">
<div class="skeleton-text skeleton-text-primary"></div>
<div class="skeleton-text skeleton-text-secondary"></div>
</div>
<div class="skeleton-cell skeleton-group"></div>
<div class="skeleton-cell skeleton-status"></div>
<div class="skeleton-cell skeleton-time"></div>
<div class="skeleton-cell skeleton-action"></div>
</div>
</div>
<!-- 数据表格 -->
<el-table
v-loading="loading"
v-else
:data="userList"
@selection-change="handleSelectionChange"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="UserId" label="用户ID" width="100" />
@@ -123,6 +153,7 @@
background
class="pagination"
/>
</div>
</el-card>
<!-- 用户编辑对话框 -->
@@ -894,22 +925,198 @@ onMounted(() => {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin-bottom: 15px;
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
min-width: 400px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
}
.search-form :deep(.el-form-item__label) {
margin-right: 8px;
white-space: nowrap;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-container {
border-radius: 8px;
@media (max-width: 768px) {
.filter-content {
flex-direction: column;
align-items: stretch;
}
.search-form {
min-width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-end;
}
}
.table-section {
padding: 0;
}
/* 骨架屏样式 */
.skeleton-container {
padding: 20px;
}
.skeleton-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.skeleton-row:last-child {
border-bottom: none;
}
.skeleton-cell {
display: flex;
align-items: center;
}
.skeleton-checkbox {
width: 55px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-id {
width: 100px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-user {
flex: 1;
min-width: 150px;
gap: 12px;
}
.skeleton-avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-text-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-text {
height: 14px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-text-primary {
width: 80px;
}
.skeleton-text-secondary {
width: 120px;
}
.skeleton-phone {
width: 130px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-realname {
width: 150px;
flex-direction: column;
gap: 8px;
}
.skeleton-group {
width: 120px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-status {
width: 100px;
height: 24px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-time {
width: 180px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-action {
width: 200px;
height: 32px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.user-info {
@@ -926,31 +1133,36 @@ onMounted(() => {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
color: #2c3e50;
}
.email {
font-size: 12px;
color: #999;
color: #7f8c8d;
}
.real-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
color: #2c3e50;
}
.id-card {
font-size: 12px;
color: #666;
color: #7f8c8d;
}
.text-gray {
color: #999;
color: #95a5a6;
font-size: 12px;
}
.pagination {
margin-top: 24px;
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
@@ -958,12 +1170,9 @@ onMounted(() => {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
.text-gray {
color: #999;
font-size: 12px;
}
.action-buttons {
display: flex;
@@ -980,13 +1189,12 @@ onMounted(() => {
.avatar-preview {
position: relative;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
transition: opacity 0.2s ease;
}
.avatar-preview:hover {
transform: scale(1.05);
opacity: 0.8;
}
.avatar-preview:hover .avatar-overlay {
@@ -1006,7 +1214,7 @@ onMounted(() => {
justify-content: center;
gap: 4px;
opacity: 0;
transition: opacity 0.3s ease;
transition: opacity 0.2s ease;
color: white;
}
@@ -1026,5 +1234,36 @@ onMounted(() => {
font-size: 14px;
color: #606266;
}
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>