4 Commits

Author SHA1 Message Date
lin 0fe4ece1a9 fit:工单修改和商品关联修改 2025-12-10 20:17:13 +08:00
wlkjyy a09631551b xx 2025-12-08 15:24:48 +08:00
wlkjyy 5ea4f2cfe3 Merge branch 'master' into qian 2025-11-28 14:17:29 +08:00
wlkjyy f7c3be1d30 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
2025-11-28 14:15:29 +08:00
55 changed files with 9829 additions and 8142 deletions
+2
View File
@@ -1,3 +1,5 @@
# 管理员后台pc端
# 007UI 后台管理系统 # 007UI 后台管理系统
一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。 一个基于Vue 3、Element Plus的现代化后台管理系统模板,采用蓝色扁平化高端设计风格。
+322 -12
View File
@@ -88,40 +88,350 @@ html, body {
background: #a8a8a8; background: #a8a8a8;
} }
/* Element Plus样式优化 */ /* Element Plus全局配色优化 */
/* 按钮扁平化 */
.el-button { .el-button {
font-weight: 400; border-radius: 0 !important;
border-radius: 4px; transition: all 0.2s ease;
font-weight: 500;
} }
/* 主按钮 - 深蓝灰色 */
.el-button--primary {
background-color: #2c3e50 !important;
border-color: #2c3e50 !important;
color: #ffffff !important;
}
.el-button--primary:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
.el-button--primary:active {
background-color: #1a252f !important;
border-color: #1a252f !important;
}
/* 成功按钮 - 绿色系 */
.el-button--success {
background-color: #27ae60 !important;
border-color: #27ae60 !important;
color: #ffffff !important;
}
.el-button--success:hover {
background-color: #2ecc71 !important;
border-color: #2ecc71 !important;
}
.el-button--success:active {
background-color: #229954 !important;
border-color: #229954 !important;
}
/* 危险按钮 - 红色系 */
.el-button--danger {
background-color: #e74c3c !important;
border-color: #e74c3c !important;
color: #ffffff !important;
}
.el-button--danger:hover {
background-color: #ec7063 !important;
border-color: #ec7063 !important;
}
.el-button--danger:active {
background-color: #c0392b !important;
border-color: #c0392b !important;
}
/* 默认按钮 */
.el-button--default {
background-color: #ffffff !important;
border-color: #d5d9e0 !important;
color: #606266 !important;
}
.el-button--default:hover {
background-color: #f5f7fa !important;
border-color: #c0c4cc !important;
color: #606266 !important;
}
/* Link按钮 */
.el-button.is-link {
color: #3498db !important;
border: none !important;
padding: 0;
background: transparent !important;
}
.el-button.is-link:hover {
color: #2980b9 !important;
background: transparent !important;
}
.el-button--primary.is-link {
color: #3498db !important;
background: transparent !important;
}
.el-button--primary.is-link:hover {
color: #2980b9 !important;
background: transparent !important;
}
/* 输入框扁平化 */
.el-input__wrapper {
border-radius: 0 !important;
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
background-color: #ffffff !important;
transition: all 0.2s ease;
}
.el-input__wrapper:hover {
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
}
.el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px #2c3e50 inset !important;
}
/* 标签扁平化 */
.el-tag {
border-radius: 0 !important;
border: none !important;
font-weight: 500;
padding: 2px 10px;
}
/* 成功标签 */
.el-tag--success {
background-color: #d5f4e6 !important;
color: #27ae60 !important;
}
/* 危险标签 */
.el-tag--danger {
background-color: #fadbd8 !important;
color: #e74c3c !important;
}
/* 信息标签 */
.el-tag--info {
background-color: #ebf5fb !important;
color: #3498db !important;
}
/* 卡片扁平化 */
.el-card { .el-card {
border-radius: 4px; border-radius: 0 !important;
border: 1px solid #e1e8ed !important;
box-shadow: none !important;
} }
/* 表格扁平化 */
.el-table {
border-radius: 0 !important;
border: none !important;
color: #2c3e50 !important;
}
.el-table__header {
background: #f8f9fa !important;
}
.el-table th {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed !important;
color: #2c3e50 !important;
font-weight: 600 !important;
font-size: 13px !important;
}
.el-table td {
border-bottom: 1px solid #f0f2f5 !important;
color: #34495e !important;
}
.el-table tr:hover > td {
background-color: #f8f9fa !important;
}
/* 分页扁平化 */
.el-pagination .el-pager li {
border-radius: 0 !important;
color: #606266 !important;
font-weight: 500;
}
.el-pagination .el-pager li.is-active {
background-color: #2c3e50 !important;
color: #ffffff !important;
}
.el-pagination .el-pager li:hover {
color: #2c3e50 !important;
}
.el-pagination button {
border-radius: 0 !important;
color: #606266 !important;
}
.el-pagination button:hover {
color: #2c3e50 !important;
}
.el-pagination .el-select .el-input__wrapper {
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
}
.el-pagination .el-input__inner {
color: #606266 !important;
}
/* 下拉菜单扁平化 */
.el-dropdown-menu {
border-radius: 0 !important;
border: 1px solid #e1e8ed !important;
background-color: #ffffff !important;
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1) !important;
padding: 4px 0;
}
.el-dropdown-menu__item {
color: #34495e !important;
transition: all 0.2s ease;
padding: 8px 16px;
}
.el-dropdown-menu__item:hover {
background-color: #f8f9fa !important;
color: #2c3e50 !important;
}
.el-dropdown-menu__item.is-divided {
border-top: 1px solid #e1e8ed !important;
}
/* 选择框扁平化 */
.el-select .el-input__wrapper {
border-radius: 0 !important;
}
/* 文本域扁平化 */
.el-textarea__inner {
border-radius: 0 !important;
box-shadow: 0 0 0 1px #d5d9e0 inset !important;
transition: all 0.2s ease;
}
.el-textarea__inner:hover {
box-shadow: 0 0 0 1px #b8bcc5 inset !important;
}
.el-textarea__inner:focus {
box-shadow: 0 0 0 1px #2c3e50 inset !important;
}
/* 菜单扁平化 */
.el-menu { .el-menu {
border-right: none; border-right: none;
} }
.el-table { /* 表单标签 */
border-radius: 4px; .el-form-item__label {
color: #2c3e50 !important;
font-weight: 500;
}
/* Dialog 扁平化样式 */
.el-overlay {
background-color: rgba(0, 0, 0, 0.5);
}
.el-overlay-dialog {
display: flex;
align-items: center;
justify-content: center;
} }
.el-dialog { .el-dialog {
border-radius: 8px; border-radius: 0;
border: none;
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.15);
background-color: #ffffff;
margin: 0;
padding: 0 !important;
}
.el-dialog__wrapper {
border: none;
} }
.el-dialog__header { .el-dialog__header {
margin: 0; margin: 0;
padding: 20px; padding: 20px 24px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #e1e8ed;
font-weight: 500; background-color: #fafbfc;
font-weight: 600;
font-size: 16px;
color: #2c3e50;
}
.el-dialog__headerbtn {
top: 20px;
right: 24px;
width: 32px;
height: 32px;
}
.el-dialog__close {
color: #7f8c8d;
font-size: 18px;
transition: all 0.2s ease;
}
.el-dialog__close:hover {
color: #2c3e50;
background-color: #f8f9fa;
} }
.el-dialog__body { .el-dialog__body {
padding: 20px; padding: 24px;
color: #34495e;
background-color: #ffffff;
} }
.el-dialog__footer { .el-dialog__footer {
padding: 10px 20px 20px; padding: 16px 24px;
border-top: 1px solid #e1e8ed;
background-color: #fafbfc;
}
/* Dialog 内表单组件样式 */
.el-dialog .el-input__wrapper {
border-radius: 0;
}
.el-dialog .el-select .el-input__wrapper {
border-radius: 0;
}
.el-dialog .el-textarea__inner {
border-radius: 0;
}
.el-dialog .el-form-item__label {
color: #2c3e50;
font-weight: 500;
}
.el-dialog .el-form-item {
margin-bottom: 20px;
} }
</style> </style>
BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+191
View File
@@ -0,0 +1,191 @@
<template>
<el-dialog
:model-value="visible"
title="选择用户"
width="800px"
class="user-selector-dialog"
append-to-body
@update:model-value="handleVisibleChange"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="searchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="handleSearch"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="loading"
:data="userList"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="searchParams.page"
v-model:page-size="searchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
background
class="selector-pagination"
/>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmSelection" :disabled="!selectedUser">
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { getUserList } from '@/api/admin/user'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'select'])
const loading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedUser = ref(null)
const searchParams = reactive({
key: '',
page: 1,
count: 10
})
// 监听 visible 变化,打开时加载数据
watch(() => props.visible, (newVal) => {
if (newVal) {
selectedUser.value = null
if (userList.value.length === 0) {
fetchUserList()
}
}
})
const handleVisibleChange = (val) => {
emit('update:visible', val)
}
const closeDialog = () => {
emit('update:visible', false)
}
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(searchParams)
if (res.data.code === 200) {
userList.value = res.data.data?.data || []
total.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
searchParams.page = 1
fetchUserList()
}
const handleReset = () => {
searchParams.key = ''
searchParams.page = 1
fetchUserList()
}
const handleCurrentChange = (row) => {
selectedUser.value = row
}
const handleSizeChange = (size) => {
searchParams.count = size
fetchUserList()
}
const handlePageChange = (page) => {
searchParams.page = page
fetchUserList()
}
const confirmSelection = () => {
if (!selectedUser.value) {
ElMessage.warning('请选择一个用户')
return
}
emit('select', selectedUser.value)
closeDialog()
}
</script>
<style scoped>
.selector-search {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
}
.selector-pagination {
margin-top: 16px;
justify-content: flex-end;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row):hover {
background-color: #f5f7fa;
}
:deep(.current-row) {
background-color: var(--el-color-primary-light-8) !important;
color: var(--el-color-primary);
font-weight: bold;
}
</style>
+156 -40
View File
@@ -3,15 +3,15 @@
<!-- 侧边栏 --> <!-- 侧边栏 -->
<div class="sidebar"> <div class="sidebar">
<div class="logo-container"> <div class="logo-container">
<h1 class="title">零零七云计算后台控制面板</h1> <img src="@/assets/logo.png" alt="Logo" class="logo-img" />
</div> </div>
<el-scrollbar class="sidebar-scrollbar"> <el-scrollbar class="sidebar-scrollbar">
<el-menu <el-menu
:default-active="activeMenu" :default-active="activeMenu"
class="sidebar-menu" class="sidebar-menu"
background-color="#ffffff" background-color="transparent"
text-color="#333333" text-color="#34495e"
active-text-color="#1890ff" active-text-color="#2c3e50"
:unique-opened="true" :unique-opened="true"
router router
> >
@@ -143,46 +143,39 @@ const handleLogout = () => {
/* 侧边栏样式 */ /* 侧边栏样式 */
.sidebar { .sidebar {
width: 240px; width: 260px;
height: 100%; height: 100%;
background-color: #ffffff; background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-right: 1px solid #e1e8ed;
overflow: hidden; overflow: hidden;
z-index: 20; z-index: 20;
} }
.logo-container { .logo-container {
height: 60px; height: 70px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 16px; justify-content: center;
color: #333; padding: 0 20px;
background-color: #ffffff; background-color: #ffffff;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #e1e8ed;
overflow: hidden;
} }
.logo { .logo-img {
width: 32px; height: 50px;
height: 32px; width: auto;
margin-right: 10px; object-fit: contain;
}
.title {
font-size: 18px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
color: #1890ff;
} }
.sidebar-scrollbar { .sidebar-scrollbar {
height: calc(100vh - 60px); height: calc(100vh - 70px);
} }
.sidebar-menu { .sidebar-menu {
border-right: none; border-right: none;
min-height: 100%; min-height: 100%;
background-color: transparent !important;
padding: 0;
} }
/* 主容器样式 */ /* 主容器样式 */
@@ -197,9 +190,9 @@ const handleLogout = () => {
/* 顶部导航栏样式 */ /* 顶部导航栏样式 */
.navbar { .navbar {
height: 60px; height: 60px;
padding: 0 15px; padding: 0 20px;
background-color: #fff; background-color: #ffffff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); border-bottom: 1px solid #e1e8ed;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -209,32 +202,35 @@ const handleLogout = () => {
.navbar-left { .navbar-left {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1;
} }
.navbar-right { .navbar-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
} }
.navbar-item { .navbar-item {
padding: 0 10px;
height: 60px; height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header-btn { .header-btn {
height: 40px; height: 36px;
width: 40px; width: 36px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #606266; color: #34495e;
transition: all 0.2s ease;
border-radius: 0;
} }
.header-btn:hover { .header-btn:hover {
background-color: #f5f7fa; background-color: #f8f9fa;
border-radius: 4px; color: #2c3e50;
} }
.avatar-container { .avatar-container {
@@ -243,16 +239,31 @@ const handleLogout = () => {
cursor: pointer; cursor: pointer;
padding: 0 12px; padding: 0 12px;
height: 60px; height: 60px;
gap: 8px;
transition: all 0.2s ease;
border-radius: 0;
} }
.avatar-container:hover { .avatar-container:hover {
background-color: rgba(0, 0, 0, 0.025); background-color: #f8f9fa;
} }
.username { .username {
margin: 0 8px; margin: 0;
font-size: 14px; 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; 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) { :deep(.el-dropdown-menu__item) {
display: flex; display: flex;
align-items: center; align-items: center;
color: #34495e;
transition: all 0.2s ease;
padding: 8px 16px;
} }
:deep(.el-dropdown-menu__item i) { :deep(.el-dropdown-menu__item i) {
margin-right: 8px; 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%; 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 菜单项样式优化 */ /* Element Plus 菜单项样式优化 */
:deep(.el-menu) { :deep(.el-menu) {
border-right: none; border-right: none;
} }
:deep(.el-sub-menu__title) { :deep(.el-sub-menu__title) {
height: 48px; height: 50px;
line-height: 48px; 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) { :deep(.el-menu-item) {
height: 48px; height: 50px;
line-height: 48px; 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> </style>
+43 -5
View File
@@ -181,34 +181,72 @@ const breadcrumbs = computed(() => {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;
height: 100%;
} }
.breadcrumb-icon { .breadcrumb-icon {
margin-right: 6px; margin-right: 6px;
font-size: 16px; font-size: 16px;
color: rgba(255, 255, 255, 0.8);
transition: color 0.2s ease;
} }
:deep(.el-breadcrumb__item) { :deep(.el-breadcrumb__item) {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
height: 100%;
} }
:deep(.el-breadcrumb__inner) { :deep(.el-breadcrumb__inner) {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
color: #7f8c8d;
font-weight: 400;
font-size: 14px;
transition: all 0.2s ease;
} }
:deep(.el-breadcrumb__inner a) { :deep(.el-breadcrumb__inner a) {
color: #606266; color: #7f8c8d;
font-weight: normal; font-weight: 400;
transition: color 0.2s ease; transition: all 0.2s ease;
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 0;
} }
:deep(.el-breadcrumb__inner a:hover) { :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) { :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> </style>
+28 -44
View File
@@ -39,65 +39,49 @@ const hasChildren = computed(() => {
</script> </script>
<style scoped> <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 { .el-icon {
margin-right: 10px; margin-right: 12px;
width: 24px; width: 20px;
height: 20px;
text-align: center; text-align: center;
color: #666666; color: #7f8c8d;
transition: color 0.2s ease;
font-size: 18px;
} }
/* 激活菜单项特效 */ .el-menu-item .el-icon, :deep(.el-sub-menu__title .el-icon) {
.el-menu-item.is-active { color: #7f8c8d !important;
position: relative; transition: color 0.2s ease;
background-color: #e6f7ff !important;
color: #1890ff !important;
font-weight: 600;
} }
.el-menu-item.is-active::before { .el-menu-item:hover .el-icon,
content: ''; :deep(.el-sub-menu__title:hover .el-icon) {
position: absolute; color: #34495e !important;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: #1890ff;
} }
:deep(.el-sub-menu.is-active > .el-sub-menu__title) { /* 激活菜单项图标 */
color: #1890ff !important; .el-menu-item.is-active .el-icon {
font-weight: 600; color: #2c3e50 !important;
} }
.el-menu-item:hover, :deep(.el-sub-menu__title:hover) { :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) {
background-color: #f5f7fa !important; color: #2c3e50 !important;
} }
/* 修复图标颜色 */ /* 菜单文字样式 */
.el-menu-item.is-active .el-icon, :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-icon) { .el-menu-item span, :deep(.el-sub-menu__title span) {
color: #1890ff !important; font-size: 14px;
letter-spacing: 0.2px;
} }
/* 修复箭头颜色 */ /* 子菜单项样式优化 */
:deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow) { :deep(.el-sub-menu .el-menu-item) {
color: #1890ff !important; font-size: 13px;
padding-left: 48px !important;
} }
/* 子菜单样式 */ :deep(.el-sub-menu .el-menu-item .el-icon) {
:deep(.el-menu--inline) { font-size: 16px;
background-color: #fafafa; margin-right: 10px;
} }
</style> </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 { useRoute, useRouter } from 'vue-router'
import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue' import { Close, Refresh, CircleClose, Back, Right, Remove } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { useTagsViewStore } from '@/store/tagsViewStore'
const router = useRouter() const router = useRouter()
const route = useRoute() 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 visible = ref(false)
const top = ref(0) const top = ref(0)
@@ -77,101 +79,67 @@ const selectedTag = ref({})
const initTags = () => { const initTags = () => {
// 如果当前路由不在访问过的标签中,添加它 // 如果当前路由不在访问过的标签中,添加它
if (route.name) { if (route.name) {
addVisitedView(route) tagsViewStore.addVisitedView(route)
} }
// 添加固定标签(仪表盘) // 添加固定标签(仪表盘)
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard') const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
if (dashboardRoute) { if (dashboardRoute) {
affixTags.value.push(dashboardRoute) // 注意:这里我们直接修改 store 的 affixTags,或者 store 应该提供一个 action
addVisitedView(dashboardRoute) // 简单起见,我们假设 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 refreshSelectedTag = (view) => {
// 路由刷新的原理是先获取当前路由的全部信息,然后将路由重定向到一个空白页,
// 然后立即再将路由重定向回原路由,实现刷新效果
const { fullPath } = view const { fullPath } = view
router.replace('/redirect' + fullPath) router.replace('/redirect' + fullPath)
} }
// 关闭选中的标签 // 关闭选中的标签
const closeSelectedTag = (view) => { const closeSelectedTag = (view) => {
// 从访问过的标签中移除 tagsViewStore.delVisitedView(view).then((visitedViews) => {
const index = visitedViews.value.findIndex(v => v.path === view.path) if (isActive(view)) {
if (index > -1) { toLastView(visitedViews, view)
visitedViews.value.splice(index, 1) }
} })
// 如果关闭的是当前标签,则跳转到下一个标签
if (isActive(view)) {
toLastView(visitedViews.value, view)
}
} }
// 关闭其他标签 // 关闭其他标签
const closeOthersTags = () => { const closeOthersTags = () => {
// 保留固定标签和当前选中的标签 router.push(selectedTag.value)
visitedViews.value = visitedViews.value.filter(v => { tagsViewStore.delOthersViews(selectedTag.value).then(() => {
return isAffix(v) || v.path === selectedTag.value.path // moveToCurrentTag() // 如果有滚动逻辑
}) })
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
} }
// 关闭左侧标签 // 关闭左侧标签
const closeLeftTags = () => { const closeLeftTags = () => {
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path) tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
if (selectedIndex === -1) return if (!visitedViews.find(i => i.path === route.path)) {
toLastView(visitedViews)
// 保留固定标签和右侧标签 }
visitedViews.value = visitedViews.value.filter((v, i) => {
return isAffix(v) || i >= selectedIndex
}) })
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
} }
// 关闭右侧标签 // 关闭右侧标签
const closeRightTags = () => { const closeRightTags = () => {
const selectedIndex = visitedViews.value.findIndex(v => v.path === selectedTag.value.path) tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
if (selectedIndex === -1) return if (!visitedViews.find(i => i.path === route.path)) {
toLastView(visitedViews)
// 保留固定标签和左侧标签 }
visitedViews.value = visitedViews.value.filter((v, i) => {
return isAffix(v) || i <= selectedIndex
}) })
if (!isActive(selectedTag.value)) {
router.push(selectedTag.value)
}
} }
// 关闭所有标签 // 关闭所有标签
const closeAllTags = () => { const closeAllTags = () => {
// 仅保留固定标签 tagsViewStore.delAllViews().then((visitedViews) => {
visitedViews.value = visitedViews.value.filter(v => isAffix(v)) toLastView(visitedViews)
})
// 跳转到第一个标签或首页
toLastView(visitedViews.value)
} }
// 跳转到最后一个标签或首页 // 跳转到最后一个标签或首页
@@ -182,7 +150,6 @@ const toLastView = (visitedViews, view) => {
} else { } else {
// 如果没有标签,则跳转到首页 // 如果没有标签,则跳转到首页
if (view && view.name === 'Dashboard') { if (view && view.name === 'Dashboard') {
// 如果当前是首页,则刷新页面
router.push('/redirect' + '/dashboard') router.push('/redirect' + '/dashboard')
} else { } else {
router.push('/') router.push('/')
@@ -197,7 +164,7 @@ const isActive = (tag) => {
// 判断是否是固定标签 // 判断是否是固定标签
const isAffix = (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) => { watch(route, (newRoute) => {
if (newRoute.name) { if (newRoute.name) {
addVisitedView(newRoute) tagsViewStore.addVisitedView(newRoute)
} }
}) })
@@ -245,9 +212,8 @@ onBeforeUnmount(() => {
.tags-view-container { .tags-view-container {
height: 40px; height: 40px;
width: 100%; width: 100%;
background-color: #fff; background-color: #ffffff;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #e1e8ed;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
z-index: 10; z-index: 10;
} }
@@ -256,7 +222,7 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 16px; padding: 0 12px;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
@@ -270,41 +236,56 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%; height: 100%;
gap: 4px;
} }
.tag, .active-tag { .tag, .active-tag {
height: 28px; height: 32px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 0 10px; padding: 0 12px;
margin-right: 5px; margin-right: 0;
border-radius: 2px; border-radius: 0;
font-size: 12px; font-size: 13px;
color: #333333;
background-color: #f4f4f5;
text-decoration: none; text-decoration: none;
position: relative; position: relative;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); transition: all 0.2s ease;
border: 1px solid #e8e8e8; border: 1px solid transparent;
border-bottom: none;
}
.tag {
color: #7f8c8d;
background-color: #f8f9fa;
} }
.tag:hover { .tag:hover {
color: #1890ff; color: #34495e;
background-color: #e6f7ff; background-color: #e8ecf0;
border-color: #1890ff;
} }
.active-tag { .active-tag {
color: #1890ff; color: #2c3e50;
background-color: #e6f7ff; background-color: #ffffff;
border-color: #1890ff; border: 1px solid #e1e8ed;
font-weight: 600; border-bottom: 2px solid #2c3e50;
font-weight: 500;
} }
.tag-icon { .tag-icon {
margin-right: 4px; margin-right: 6px;
width: 14px; width: 14px;
height: 14px; height: 14px;
color: #95a5a6;
transition: color 0.2s ease;
}
.tag:hover .tag-icon {
color: #34495e;
}
.active-tag .tag-icon {
color: #2c3e50;
} }
.tag-title { .tag-title {
@@ -315,36 +296,46 @@ onBeforeUnmount(() => {
} }
.tag-close { .tag-close {
margin-left: 5px; margin-left: 8px;
width: 14px; width: 16px;
height: 14px; height: 16px;
border-radius: 50%; border-radius: 0;
transition: all 0.3s; 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 { .tag:hover .tag-close {
color: #666; color: #7f8c8d;
background-color: rgba(0, 0, 0, 0.1);
} }
.active-tag .tag-close { .active-tag .tag-close {
color: #1890ff; color: #7f8c8d;
} }
.active-tag:hover .tag-close { .active-tag .tag-close:hover {
background-color: rgba(24, 144, 255, 0.1); color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
} }
.contextmenu { .contextmenu {
position: fixed; position: fixed;
z-index: 100; z-index: 100;
background-color: #fff; background-color: #ffffff;
list-style-type: none; list-style-type: none;
padding: 6px 0; padding: 4px 0;
border-radius: 4px; border-radius: 0;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(44, 62, 80, 0.1);
font-size: 12px; font-size: 12px;
border: 1px solid #ebeef5; border: 1px solid #e1e8ed;
} }
.contextmenu li { .contextmenu li {
@@ -352,15 +343,22 @@ onBeforeUnmount(() => {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
color: #34495e;
transition: all 0.2s ease;
} }
.contextmenu li:hover { .contextmenu li:hover {
background-color: #f5f7fa; background-color: #f8f9fa;
color: #1890ff; color: #2c3e50;
} }
.contextmenu li .el-icon { .contextmenu li .el-icon {
margin-right: 8px; margin-right: 8px;
font-size: 14px; font-size: 14px;
color: #7f8c8d;
}
.contextmenu li:hover .el-icon {
color: #2c3e50;
} }
</style> </style>
+29 -61
View File
@@ -5,20 +5,20 @@ export const menus = [
icon: 'DataBoard' icon: 'DataBoard'
}, },
{ {
path : '/ticket', path: '/ticket',
title: '工单处理', title: '工单处理',
icon: 'DataBoard' icon: 'DataBoard'
}, },
{ {
path:'/user', path: '/user',
title: '用户管理', title: '用户管理',
icon: 'User', icon: 'User',
children: [ children: [
{ {
path: '/user/list', path: '/user/list',
title: '用户列表' title: '用户列表'
}, },
{ {
path: '/user/balance', path: '/user/balance',
title: '用户余额管理' title: '用户余额管理'
}, },
@@ -45,10 +45,7 @@ export const menus = [
path: '/product/group', path: '/product/group',
title: '商品分组' title: '商品分组'
}, },
{
path: '/product/parameter',
title: '商品参数'
}
] ]
}, },
{ {
@@ -75,36 +72,7 @@ export const menus = [
path: '/marketing/voucher', path: '/marketing/voucher',
title: '代金券管理' title: '代金券管理'
}, },
{
path: '/marketing/user-distribution',
title: '用户分发管理'
},
{
id: 'discount-goods',
title: '商品关联管理',
path: '/marketing/discount-goods',
badge: 'NEW'
},
{
id: 'discount-users',
title: '用户关联管理',
path: '/marketing/discount-users',
badge: 'NEW'
},
{
id: 'user-info',
title: '用户信息管理',
path: '/marketing/user-info',
badge: 'NEW'
},
{
id: 'user-history',
title: '用户使用记录管理',
path: '/marketing/user-history',
badge: 'NEW'
}
] ]
}, },
{ {
@@ -141,9 +109,9 @@ export const menus = [
{ path: '/acs/images/categories', title: '镜像分类' } { path: '/acs/images/categories', title: '镜像分类' }
] ]
}, },
{ {
path: '/acs/nodes', path: '/acs/nodes',
title: '节点管理' title: '节点管理'
}, },
{ {
path: '/acs/guacamole', path: '/acs/guacamole',
@@ -158,10 +126,10 @@ export const menus = [
] ]
}, },
{ {
path:'/setting', path: '/setting',
title:'全局设置管理', title: '全局设置管理',
children:[ children: [
{path:'/setting/global',title:'全局设置'} { path: '/setting/global', title: '全局设置' }
] ]
} }
] ]
@@ -171,31 +139,31 @@ export const menus = [
title: '系统管理', title: '系统管理',
icon: 'Setting', icon: 'Setting',
children: [ children: [
{ {
path: '/system/permission', path: '/system/permission',
title: '权限管理', title: '权限管理',
children: [ children: [
{ path: '/system/permission/route', title: '路由权限' }, { path: '/system/permission/route', title: '路由权限' },
{ path: '/system/permission/admin', title: '管理员权限' } { path: '/system/permission/admin', title: '管理员权限' }
] ]
}, },
{ {
path: '/system/file', path: '/system/file',
title: '文件管理' title: '文件管理'
}, },
{ {
path: '/system/domain-whitelist', path: '/system/domain-whitelist',
title: '域名白名单' title: '域名白名单'
}, },
{ {
path: '/system/setting-group', path: '/system/setting-group',
title: '配置组管理' title: '配置组管理'
}, },
{ {
path: '/system/setting-list', path: '/system/setting-list',
title: '配置管理' title: '配置管理'
} }
] ]
} }
+39 -67
View File
@@ -41,7 +41,7 @@ const routes = [
}, },
component: () => import('../views/ticket/TicketChat.vue'), component: () => import('../views/ticket/TicketChat.vue'),
}, },
// ACS管理路由 // ACS管理路由
{ {
path: 'acs', path: 'acs',
@@ -130,7 +130,19 @@ const routes = [
meta: { meta: {
title: '节点管理' title: '节点管理'
} }
},{ },
{
path: 'nodes/form',
name: 'ServerForm',
component: () => import('@/views/acs/nodes/ServerForm.vue'),
meta: { title: '服务器表单', activeMenu: '/acs/nodes', hidden: true }
},
{
path: 'images/form',
name: 'ImageForm',
component: () => import('@/views/acs/images/ImageForm.vue'),
meta: { title: '镜像表单', activeMenu: '/acs/images/vm', hidden: true }
}, {
path: 'guacamole', path: 'guacamole',
name: 'Guacamole', name: 'Guacamole',
component: () => import('../views/acs/guacamole/Guacamole.vue'), component: () => import('../views/acs/guacamole/Guacamole.vue'),
@@ -218,14 +230,7 @@ const routes = [
title: '商品分组' title: '商品分组'
} }
}, },
{
path: 'parameter',
name: 'ProductParameter',
component: () => import('../views/product/ProductParameter.vue'),
meta: {
title: '商品参数'
}
}
] ]
}, },
// 订单管理路由 // 订单管理路由
@@ -275,49 +280,16 @@ const routes = [
} }
}, },
{ {
path: 'user-distribution', path: 'voucher/:id/manage',
name: 'UserDistribution', name: 'VoucherManagement',
component: () => import('../views/marketing/UserVoucher.vue'), component: () => import('../views/marketing/VoucherManagement.vue'),
meta: { meta: {
title: '用户分发管理' title: '代金券详情管理',
hidden: true,
activeMenu: '/marketing/voucher'
} }
}, },
{
path: 'discount-goods',
name: 'DiscountGoods',
component: () => import('../views/marketing/DiscountGoods.vue'),
meta: {
title: '商品关联管理',
badge: 'NEW'
}
},
{
path: 'discount-users',
name: 'DiscountUsers',
component: () => import('../views/marketing/DiscountUsers.vue'),
meta: {
title: '用户关联管理',
badge: 'NEW'
}
},
{
path: 'user-info',
name: 'UserInfo',
component: () => import('../views/marketing/VoucherHolders.vue'),
meta: {
title: '用户信息管理',
badge: 'NEW'
}
},
{
path: 'user-history',
name: 'UserHistory',
component: () => import('../views/marketing/VoucherHistory.vue'),
meta: {
title: '用户使用记录管理',
badge: 'NEW'
}
}
] ]
}, },
// 活动管理路由 // 活动管理路由
@@ -365,7 +337,7 @@ const routes = [
title: '管理员权限' title: '管理员权限'
} }
}, },
{ {
path: 'file', path: 'file',
name: 'SystemFile', name: 'SystemFile',
@@ -374,7 +346,7 @@ const routes = [
title: '文件管理' title: '文件管理'
} }
}, },
{ {
path: 'domain-whitelist', path: 'domain-whitelist',
name: 'DomainWhitelist', name: 'DomainWhitelist',
@@ -492,21 +464,21 @@ const routes = [
title: '容器详情', title: '容器详情',
hidden: true hidden: true
} }
},{ }, {
path:'servers/container/console', path: 'servers/container/console',
name:'ContainerConsole', name: 'ContainerConsole',
component:()=>import('../views/acs/nodes/containerConsole.vue'), component: () => import('../views/acs/nodes/containerConsole.vue'),
meta:{ meta: {
title:'终端容器', title: '终端容器',
hidden:true hidden: true
} }
},{ }, {
path:'servers/container/files', path: 'servers/container/files',
name:'ContainerFiles', name: 'ContainerFiles',
component:()=>import('../views/acs/nodes/containFile.vue'), component: () => import('../views/acs/nodes/containFile.vue'),
meta:{ meta: {
title:'容器文件管理', title: '容器文件管理',
hidden:true hidden: true
} }
} }
] ]
@@ -540,7 +512,7 @@ const router = createRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// 设置页面标题 // 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统' document.title = to.meta.title ? `${to.meta.title} - 007UI管理系统` : '007UI管理系统'
// 这里可以添加登录验证逻辑 // 这里可以添加登录验证逻辑
const isAuthenticated = localStorage.getItem('token') const isAuthenticated = localStorage.getItem('token')
if (to.path !== '/login' && !isAuthenticated) { if (to.path !== '/login' && !isAuthenticated) {
+91
View File
@@ -0,0 +1,91 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useTagsViewStore = defineStore('tagsView', () => {
const visitedViews = ref([])
const affixTags = ref([])
// 添加访问过的标签
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 delVisitedView = (view) => {
return new Promise((resolve) => {
const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index > -1) {
visitedViews.value.splice(index, 1)
}
resolve([...visitedViews.value])
})
}
// 删除其他标签
const delOthersViews = (view) => {
return new Promise((resolve) => {
visitedViews.value = visitedViews.value.filter(v => {
return v.meta.affix || v.path === view.path
})
resolve([...visitedViews.value])
})
}
// 删除所有标签
const delAllViews = () => {
return new Promise((resolve) => {
visitedViews.value = visitedViews.value.filter(tag => tag.meta.affix)
resolve([...visitedViews.value])
})
}
// 删除左侧标签
const delLeftViews = (view) => {
return new Promise((resolve) => {
const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index === -1) {
resolve([...visitedViews.value])
return
}
visitedViews.value = visitedViews.value.filter((v, i) => {
return v.meta.affix || i >= index
})
resolve([...visitedViews.value])
})
}
// 删除右侧标签
const delRightViews = (view) => {
return new Promise((resolve) => {
const index = visitedViews.value.findIndex(v => v.path === view.path)
if (index === -1) {
resolve([...visitedViews.value])
return
}
visitedViews.value = visitedViews.value.filter((v, i) => {
return v.meta.affix || i <= index
})
resolve([...visitedViews.value])
})
}
return {
visitedViews,
affixTags,
addVisitedView,
delVisitedView,
delOthersViews,
delAllViews,
delLeftViews,
delRightViews
}
})
+88
View File
@@ -0,0 +1,88 @@
[
{
"value": "beijing",
"label": "北京",
"children": [
{
"value": "beijing",
"label": "北京"
}
]
},
{
"value": "shanghai",
"label": "上海",
"children": [
{
"value": "shanghai",
"label": "上海"
}
]
},
{
"value": "guangdong",
"label": "广东",
"children": [
{
"value": "guangzhou",
"label": "广州"
},
{
"value": "shenzhen",
"label": "深圳"
}
]
},
{
"value": "zhejiang",
"label": "浙江",
"children": [
{
"value": "hangzhou",
"label": "杭州"
}
]
},
{
"value": "jiangsu",
"label": "江苏",
"children": [
{
"value": "nanjing",
"label": "南京"
},
{
"value": "suzhou",
"label": "苏州"
}
]
},
{
"value": "hongkong",
"label": "香港",
"children": [
{
"value": "hongkong",
"label": "香港"
}
]
},
{
"value": "overseas",
"label": "海外",
"children": [
{
"value": "usa",
"label": "美国"
},
{
"value": "japan",
"label": "日本"
},
{
"value": "singapore",
"label": "新加坡"
}
]
}
]
+5
View File
@@ -51,4 +51,9 @@ export function timeToTimestamp(time) {
} }
return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000 return Math.floor(timestamp / 1000); // 返回毫秒级时间戳(如 1751107200000
}
export function reducenum(num){
return num / 100
} }
+166 -179
View File
@@ -1,17 +1,5 @@
<template> <template>
<div class="guacamole-container"> <div class="guacamole-container">
<!-- 页面标题和操作按钮 -->
<div class="page-header">
<div class="left">
<h2 class="title">远程桌面网关管理</h2>
<el-tag type="info" effect="plain" class="count-tag"> {{ guacamoleStats.total }} 个配置</el-tag>
</div>
<div class="actions">
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">添加配置</el-button>
<el-button @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
</div>
</div>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-panel"> <div class="stats-panel">
<div class="stat-card total-card"> <div class="stat-card total-card">
@@ -37,79 +25,91 @@
</div> </div>
</div> </div>
<!-- 搜索和筛选 --> <el-card class="main-container" shadow="never">
<div class="filter-section"> <!-- 搜索和筛选 -->
<el-input <div class="filter-section">
v-model="filterForm.url" <div class="filter-content">
placeholder="搜索 Guacamole URL" <el-form :inline="true" :model="filterForm" class="search-form">
prefix-icon="Search" <el-form-item>
clearable <el-input
@keyup.enter="handleSearch" v-model="filterForm.url"
class="search-input" placeholder="搜索 Guacamole URL"
/> prefix-icon="Search"
<div class="filter-actions"> clearable
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button> @keyup.enter="handleSearch"
<el-button @click="resetFilter" :icon="Delete">重置</el-button> style="width: 300px"
</div> />
</div> </el-form-item>
<el-form-item>
<!-- Guacamole 配置列表 --> <el-button type="primary" @click="handleSearch">
<div class="table-container"> <el-icon><search /></el-icon>搜索
<el-table </el-button>
v-loading="loading" <el-button @click="resetFilter">
:data="guacamoleData" <el-icon><refresh /></el-icon>重置
border </el-button>
stripe </el-form-item>
style="width: 100%" </el-form>
table-layout="auto" <div class="action-bar">
class="guacamole-table" <el-button type="primary" @click="handleAdd">
> <el-icon><plus /></el-icon>添加配置
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
<el-table-column label="密码" width="120" align="center">
<template #default="scope">
<el-button
type="text"
size="small"
@click="togglePasswordVisibility(scope.row)"
:icon="scope.row.showPassword ? View : Hide"
>
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
</el-button> </el-button>
</template> <el-button @click="handleRefresh">
</el-table-column> <el-icon><refresh /></el-icon>刷新
<el-table-column label="创建时间" width="180" align="center"> </el-button>
<template #default="scope"> </div>
{{ formatDate(scope.row.created_at) }} </div>
</template> </div>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="编辑配置" placement="top" :hide-after="1500">
<el-button
type="warning"
:icon="Edit"
circle
@click="handleEdit(scope.row)"
/>
</el-tooltip>
<el-tooltip content="删除配置" placement="top" :hide-after="1500">
<el-button
type="danger"
:icon="Delete"
circle
@click="handleDelete(scope.row)"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 --> <!-- Guacamole 配置列表 -->
<div class="pagination-container"> <div class="table-section">
<el-table
v-loading="loading"
:data="guacamoleData"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="ID" width="80" show-overflow-tooltip />
<el-table-column prop="url" label="Guacamole URL" min-width="200" show-overflow-tooltip />
<el-table-column prop="username" label="用户名" min-width="120" show-overflow-tooltip />
<el-table-column label="密码" width="120" align="center">
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click="togglePasswordVisibility(scope.row)"
>
<el-icon style="margin-right: 4px"><component :is="scope.row.showPassword ? View : Hide" /></el-icon>
{{ scope.row.showPassword ? scope.row.password : '••••••••' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180" align="center">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button
type="primary"
link
@click="handleEdit(scope.row)"
>
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <el-pagination
v-model:current-page="pagination.currentPage" v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize" v-model:page-size="pagination.pageSize"
@@ -119,9 +119,10 @@
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
background background
class="pagination"
/> />
</div> </div>
</div> </el-card>
<!-- 添加/编辑配置对话框 --> <!-- 添加/编辑配置对话框 -->
<el-dialog <el-dialog
@@ -510,42 +511,7 @@ onMounted(async () => {
<style scoped> <style scoped>
.guacamole-container { .guacamole-container {
padding: 20px; padding: 0;
min-height: calc(100vh - 120px);
background-color: #f5f7fa;
}
/* 页面标题样式 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.count-tag {
font-size: 13px;
}
.page-header .actions {
display: flex;
gap: 12px;
align-items: center;
} }
/* 统计卡片 */ /* 统计卡片 */
@@ -558,18 +524,18 @@ onMounted(async () => {
.stat-card { .stat-card {
background: white; background: white;
border-radius: 8px; border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 20px; padding: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid #ebeef5; border: 1px solid #e1e8ed;
} }
.stat-card:hover { .stat-card:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.stat-icon { .stat-icon {
@@ -608,60 +574,64 @@ onMounted(async () => {
font-weight: 600; font-weight: 600;
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1.1; line-height: 1.1;
color: #303133;
} }
.stat-label { .stat-label {
font-size: 14px; font-size: 14px;
color: #606266; color: #909399;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
} }
/* 搜索和筛选部分 */
.filter-section { .filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
gap: 16px; justify-content: space-between;
margin-bottom: 24px;
align-items: center; align-items: center;
background: white; padding: 16px 20px;
padding: 16px; gap: 20px;
border-radius: 8px; flex-wrap: wrap;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.search-input { .search-form {
margin: 0;
flex: 1; flex: 1;
max-width: 400px;
}
.filter-actions {
display: flex; display: flex;
gap: 8px; align-items: center;
gap: 12px;
flex-wrap: wrap;
} }
/* 表格容器 */ .search-form :deep(.el-form-item) {
.table-container { margin-bottom: 0;
background: white; margin-right: 12px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 16px;
margin-bottom: 24px;
} }
.guacamole-table { .action-bar {
margin-bottom: 16px;
}
/* 操作按钮 */
.action-buttons {
display: flex; display: flex;
justify-content: center; gap: 12px;
gap: 8px; flex-shrink: 0;
} }
/* 分页 */ .table-section {
.pagination-container { padding: 0;
display: flex; }
justify-content: flex-end;
.pagination {
margin-top: 20px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
} }
/* 表单样式 */ /* 表单样式 */
@@ -676,14 +646,6 @@ onMounted(async () => {
line-height: 1.2; line-height: 1.2;
} }
.form-section-title {
font-weight: 600;
margin: 16px 0 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
color: #409EFF;
}
/* 对话框底部 */ /* 对话框底部 */
.dialog-footer { .dialog-footer {
display: flex; display: flex;
@@ -696,6 +658,37 @@ onMounted(async () => {
gap: 8px; gap: 8px;
} }
/* 表格样式优化 */
: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;
}
/* 响应式设计 */ /* 响应式设计 */
@media screen and (max-width: 992px) { @media screen and (max-width: 992px) {
.stats-panel { .stats-panel {
@@ -708,17 +701,6 @@ onMounted(async () => {
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.page-header .actions {
width: 100%;
flex-wrap: wrap;
}
.stats-panel { .stats-panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -727,13 +709,18 @@ onMounted(async () => {
grid-column: auto; grid-column: auto;
} }
.filter-section { .filter-content {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.search-input { .search-form {
max-width: none; width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-start;
} }
} }
</style> </style>
+118 -172
View File
@@ -1,61 +1,59 @@
<template> <template>
<div class="container-images-container" v-loading="loading"> <div class="container-images-container" v-loading="loading">
<div class="page-header"> <el-card class="main-container" shadow="never">
<h2>容器镜像</h2> <!-- 搜索和操作栏 -->
<div class="header-actions"> <div class="filter-section">
<el-button type="primary" @click="handleRefresh"> <div class="filter-content">
<el-icon><refresh /></el-icon>刷新 <el-form :inline="true" :model="searchForm" class="search-form">
</el-button> <el-form-item label="服务器">
</div> <el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
</div> <el-option
v-for="server in serverList"
<!-- 服务器选择和搜索区域 --> :key="server.server_id"
<el-card class="search-card"> :label="server.name"
<el-form :inline="true" :model="searchForm" class="search-form"> :value="server.server_id"
<el-form-item label="服务器"> />
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px"> </el-select>
<el-option </el-form-item>
v-for="server in serverList" <el-form-item label="镜像名称">
:key="server.server_id" <el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
:label="server.name" </el-form-item>
:value="server.server_id" <el-form-item>
/> <el-button type="primary" @click="handleSearch">
</el-select> <el-icon><search /></el-icon>搜索
</el-form-item> </el-button>
<el-form-item label="镜像名称"> <el-button @click="resetSearch">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable /> <el-icon><refresh /></el-icon>重置
</el-form-item> </el-button>
<el-form-item> </el-form-item>
<el-button type="primary" @click="handleSearch"> </el-form>
<el-icon><search /></el-icon>搜索 <div class="action-bar">
</el-button> <el-button type="primary" @click="handleRefresh">
<el-button @click="resetSearch"> <el-icon><refresh /></el-icon>刷新
<el-icon><refresh /></el-icon>重置 </el-button>
</el-button> </div>
</el-form-item>
</el-form>
</el-card>
<!-- 当前服务器镜像列表 -->
<div v-if="currentServer" class="server-section">
<div class="server-header">
<h3>{{ currentServer.name }}</h3>
<div class="server-actions">
<el-button type="primary" @click="handleAdd(currentServer.server_id)">
<el-icon><plus /></el-icon>上传镜像
</el-button>
<el-button type="success" @click="TosyncMirror(currentServer.server_id)">
<el-icon><refresh /></el-icon>同步镜像
</el-button>
</div> </div>
</div> </div>
<el-card class="table-card"> <!-- 当前服务器镜像列表 -->
<div v-if="currentServer" class="table-section">
<div class="server-header">
<h3>{{ currentServer.name }}</h3>
<div class="server-actions">
<el-button type="primary" @click="handleAdd(currentServer.server_id)">
<el-icon><plus /></el-icon>上传镜像
</el-button>
<el-button type="success" @click="TosyncMirror(currentServer.server_id)">
<el-icon><refresh /></el-icon>同步镜像
</el-button>
</div>
</div>
<el-table <el-table
:data="currentMirrorList" :data="currentMirrorList"
border
style="width: 100%" style="width: 100%"
row-key="image_id" row-key="image_id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="镜像信息" min-width="250"> <el-table-column label="镜像信息" min-width="250">
@@ -65,7 +63,7 @@
<div class="image-info-content"> <div class="image-info-content">
<div class="image-name-row"> <div class="image-name-row">
<span class="table-image-name">{{ scope.row.name }}</span> <span class="table-image-name">{{ scope.row.name }}</span>
<el-tag>{{ scope.row.tag || '无标签' }}</el-tag> <el-tag size="small">{{ scope.row.tag || '无标签' }}</el-tag>
</div> </div>
<div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div> <div class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
</div> </div>
@@ -104,17 +102,17 @@
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-container"> <el-pagination
<el-pagination v-model:current-page="currentPage"
v-model:current-page="currentPage" :page-size="10"
:page-size="10" :total="total"
:total="total" layout="prev, pager, next"
layout="prev, pager, next" @current-change="handleCurrentPageChange"
@current-change="handleCurrentPageChange" background
/> class="pagination"
</div> />
</el-card> </div>
</div> </el-card>
<!-- 镜像详情对话框 --> <!-- 镜像详情对话框 -->
<el-dialog <el-dialog
@@ -201,12 +199,6 @@
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- <el-form-item label="分类ID" prop="class_id">
<el-input v-model="form.class_id" placeholder="请输入分类ID" />
</el-form-item>
<el-form-item label="分类名称" prop="class_name">
<el-input v-model="form.class_name" placeholder="请输入分类名称" />
</el-form-item> -->
<el-form-item label="图标"> <el-form-item label="图标">
<div class="image-icon-upload"> <div class="image-icon-upload">
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" /> <img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
@@ -986,65 +978,65 @@ onMounted(() => {
<style scoped> <style scoped>
.container-images-container { .container-images-container {
padding: 24px; padding: 0;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
} }
.page-header { .main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 24px; padding: 16px 20px;
background: #fff; gap: 20px;
padding: 16px 24px; flex-wrap: wrap;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.page-header h2 {
margin: 0;
font-size: 22px;
color: #303133;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 12px;
}
.search-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.search-form { .search-form {
margin: 0;
flex: 1;
display: flex; display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px;
padding: 8px;
} }
.server-section { .search-form :deep(.el-form-item) {
margin-bottom: 32px; margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
} }
.server-header { .server-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 16px; padding: 16px 20px;
border-bottom: 1px solid #e1e8ed;
background: #fff; background: #fff;
padding: 16px 24px;
border-radius: 8px 8px 0 0;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.server-header h3 { .server-header h3 {
margin: 0; margin: 0;
font-size: 18px; font-size: 16px;
color: #303133; color: #303133;
font-weight: 600; font-weight: 600;
display: flex; display: flex;
@@ -1055,7 +1047,7 @@ onMounted(() => {
content: ''; content: '';
display: inline-block; display: inline-block;
width: 4px; width: 4px;
height: 18px; height: 16px;
background-color: #409EFF; background-color: #409EFF;
margin-right: 10px; margin-right: 10px;
border-radius: 2px; border-radius: 2px;
@@ -1066,12 +1058,6 @@ onMounted(() => {
gap: 12px; gap: 12px;
} }
.table-card {
margin-bottom: 24px;
border-radius: 0 0 8px 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.image-info-cell { .image-info-cell {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1118,11 +1104,12 @@ onMounted(() => {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.pagination-container { .pagination {
margin-top: 20px; margin-top: 20px;
display: flex; padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
padding: 0 16px 16px;
} }
/* 详情对话框样式 */ /* 详情对话框样式 */
@@ -1263,75 +1250,34 @@ onMounted(() => {
background-color: #ecf5ff; background-color: #ecf5ff;
} }
/* 对话框样式优化 */ /* 表格样式优化 */
:deep(.el-dialog__header) {
border-bottom: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-dialog__body) {
padding: 24px;
}
:deep(.el-dialog__footer) {
border-top: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-table) { :deep(.el-table) {
border-radius: 8px; border: none;
overflow: hidden; color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
} }
:deep(.el-table th) { :deep(.el-table th) {
background-color: #f5f7fa; background: #f8f9fa !important;
color: #606266; border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600; font-weight: 600;
font-size: 13px;
} }
:deep(.el-table__row:hover) { :deep(.el-table td) {
background-color: #ecf5ff !important; border-bottom: 1px solid #f0f2f5;
color: #34495e;
} }
:deep(.el-button--link) { :deep(.el-table tr:hover > td) {
padding: 4px 8px; background-color: #f8f9fa !important;
} }
:deep(.el-button--link):hover { :deep(.el-card__body) {
background-color: #f0f2f5; padding: 0;
border-radius: 4px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.server-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.server-actions {
width: 100%;
}
.image-info-cell {
flex-direction: column;
align-items: flex-start;
}
.image-info-content {
margin-left: 0;
margin-top: 12px;
width: 100%;
}
.table-image-logo {
width: 60px;
height: 60px;
}
} }
</style> </style>
+146 -172
View File
@@ -1,107 +1,105 @@
<template> <template>
<div class="image-categories-container" v-loading="loading"> <div class="image-categories-container" v-loading="loading">
<!-- 页面标题 --> <el-card class="main-container" shadow="never">
<div class="page-header"> <!-- 搜索和操作栏 -->
<h2>镜像分类管理</h2> <div class="filter-section">
<p class="page-description">管理不同服务器下的镜像分类</p> <div class="filter-content">
</div> <el-form :inline="true" class="search-form">
<el-form-item label="服务器">
<!-- 操作栏 --> <el-select
<el-card class="search-card"> v-model="selectedServer"
<el-form :inline="true" class="search-form"> placeholder="请选择服务器"
<el-form-item label="服务器"> clearable
<el-select @change="handleServerChange"
v-model="selectedServer" style="width: 220px"
placeholder="请选择服务器" >
clearable <el-option
@change="handleServerChange" v-for="item in serverList"
style="width: 220px" :key="item.server_id"
> :label="item.name"
<el-option :value="item.server_id"
v-for="item in serverList" />
:key="item.server_id" </el-select>
:label="item.name" </el-form-item>
:value="item.server_id" <el-form-item label="分类名称">
/> <el-input
</el-select> v-model="searchKey"
</el-form-item> placeholder="搜索分类名称"
<el-form-item label="分类名称"> clearable
<el-input @input="handleSearch"
v-model="searchKey" style="width: 200px"
placeholder="搜索分类名称" />
clearable </el-form-item>
@input="handleSearch" <el-form-item>
/> <el-button type="primary" @click="handleSearch">
</el-form-item> <el-icon><search /></el-icon>搜索
<el-form-item> </el-button>
<el-button </el-form-item>
type="primary" </el-form>
@click="handleAddCategory" <div class="action-bar">
:disabled="!selectedServer"
>
<el-icon><plus /></el-icon>添加分类
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="filteredCategoryList"
style="width: 100%"
border
stripe
highlight-current-row
>
<el-table-column type="index" width="60" align="center" label="序号" />
<el-table-column prop="name" label="分类名称" min-width="150" />
<el-table-column label="分类图标" align="center" width="100">
<template #default="scope">
<el-avatar
v-if="scope.row.class_ico"
:size="40"
:src="scope.row.class_ico"
fit="cover"
/>
<el-icon v-else :size="20"><picture /></el-icon>
</template>
</el-table-column>
<el-table-column label="所属服务器" min-width="150">
<template #default="scope">
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="180">
<template #default="scope">
{{ scope.row.created_at }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="scope">
<el-button <el-button
type="success" type="primary"
link @click="handleAddCategory"
@click="handleEditCategory(scope.row)" :disabled="!selectedServer"
> >
<el-icon><edit /></el-icon>编辑 <el-icon><plus /></el-icon>添加分类
</el-button> </el-button>
</template> </div>
</el-table-column> </div>
</el-table> </div>
<!-- 分页器 --> <!-- 表格 -->
<div class="pagination-container"> <div class="table-section">
<el-table
v-loading="loading"
:data="filteredCategoryList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="index" width="60" align="center" label="序号" />
<el-table-column prop="name" label="分类名称" min-width="150" />
<el-table-column label="分类图标" align="center" width="100">
<template #default="scope">
<el-avatar
v-if="scope.row.class_ico"
:size="40"
:src="scope.row.class_ico"
fit="cover"
style="background-color: #f5f7fa; border: 1px solid #ebeef5;"
/>
<el-icon v-else :size="20" color="#909399"><picture /></el-icon>
</template>
</el-table-column>
<el-table-column label="所属服务器" min-width="150">
<template #default="scope">
<el-tag type="info">{{ getServerName(scope.row.server_id) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="180" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
@click="handleEditCategory(scope.row)"
>
<el-icon><edit /></el-icon>编辑
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
<el-pagination <el-pagination
background background
:current-page="currentPage" :current-page="currentPage"
:page-size="pageSize" :page-size="pageSize"
:page-sizes="[10, 20, 50, 100]" :page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next" layout="total, sizes, prev, pager, next, jumper"
:total="totalCount" :total="totalCount"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
class="pagination"
/> />
</div> </div>
</el-card> </el-card>
@@ -495,58 +493,59 @@ const getServerName = (serverId) => {
<style scoped> <style scoped>
.image-categories-container { .image-categories-container {
padding: 24px; padding: 0;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
} }
.page-header { .main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
margin-bottom: 24px; align-items: center;
background: #fff; padding: 16px 20px;
padding: 16px 24px; gap: 20px;
border-radius: 8px; flex-wrap: wrap;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.page-header h2 {
margin: 0;
font-size: 22px;
color: #303133;
font-weight: 600;
}
.page-description {
margin-top: 8px;
color: #6b7280;
font-size: 14px;
}
.search-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.search-form { .search-form {
margin: 0;
flex: 1;
display: flex; display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px;
padding: 8px;
} }
.table-card { .search-form :deep(.el-form-item) {
margin-bottom: 24px; margin-bottom: 0;
border-radius: 8px; margin-right: 12px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.pagination-container { .action-bar {
display: flex; display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
margin-top: 16px;
padding: 0 16px 16px;
} }
/* 图片上传区域样式 */ /* 图片上传区域样式 */
@@ -650,59 +649,34 @@ const getServerName = (serverId) => {
background-color: #ecf5ff; background-color: #ecf5ff;
} }
/* 对话框样式优化 */ /* 表格样式优化 */
:deep(.el-dialog__header) {
border-bottom: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-dialog__body) {
padding: 24px;
}
:deep(.el-dialog__footer) {
border-top: 1px solid #ebeef5;
padding: 16px 20px;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-table) { :deep(.el-table) {
border-radius: 8px; border: none;
overflow: hidden; color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
} }
:deep(.el-table th) { :deep(.el-table th) {
background-color: #f5f7fa; background: #f8f9fa !important;
color: #606266; border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600; font-weight: 600;
font-size: 13px;
} }
:deep(.el-table__row:hover) { :deep(.el-table td) {
background-color: #ecf5ff !important; border-bottom: 1px solid #f0f2f5;
color: #34495e;
} }
:deep(.el-button--link) { :deep(.el-table tr:hover > td) {
padding: 4px 8px; background-color: #f8f9fa !important;
} }
:deep(.el-button--link):hover { :deep(.el-card__body) {
background-color: #f0f2f5; padding: 0;
border-radius: 4px;
} }
</style>
/* 响应式调整 */
@media (max-width: 768px) {
.image-icon-upload {
flex-direction: column;
align-items: flex-start;
}
.upload-buttons {
margin-top: 12px;
width: 100%;
}
}
</style>
+825
View File
@@ -0,0 +1,825 @@
<template>
<div class="image-form-container">
<!-- 顶部导航 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" class="back-btn" circle>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<div class="header-title-area">
<h1 class="page-title">{{ isEdit ? '编辑镜像' : '添加镜像' }}</h1>
<span class="page-subtitle">{{ isEdit ? '修改现有镜像的配置信息' : '上传并配置新的虚拟机镜像' }}</span>
</div>
</div>
<div class="header-actions">
<el-button @click="goBack" size="large">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting" size="large" class="submit-btn">
{{ isEdit ? '保存修改' : '立即创建' }}
</el-button>
</div>
</div>
<!-- 主表单区域 -->
<div class="form-wrapper">
<el-form :model="form" label-position="top" :rules="rules" ref="formRef" class="main-form" size="large">
<!-- 左侧主要配置 -->
<div class="form-main-col">
<el-card class="premium-card" shadow="never">
<div class="section-header">
<div class="section-icon"><el-icon><Monitor /></el-icon></div>
<div class="section-info">
<h3>基础信息</h3>
<p>配置镜像的基本标识与文件信息</p>
</div>
</div>
<div class="form-grid-2">
<el-form-item label="镜像名称" prop="name">
<el-input v-model="form.name" placeholder="请输入镜像名称" />
</el-form-item>
<el-form-item label="展示名称" prop="show_name">
<el-input v-model="form.show_name" placeholder="请输入展示名称" />
</el-form-item>
</div>
<el-form-item label="文件路径" prop="path">
<el-input v-model="form.path" placeholder="请输入镜像文件在服务器上的绝对路径">
<template #prefix><el-icon><Folder /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="镜像描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入关于此镜像的详细描述"
resize="none"
/>
</el-form-item>
<el-divider />
<div class="section-header">
<div class="section-icon"><el-icon><SetUp /></el-icon></div>
<div class="section-info">
<h3>分类与版本</h3>
<p>管理镜像的分类归属与版本信息</p>
</div>
</div>
<div class="form-grid-2">
<el-form-item label="所属分类" prop="class_id">
<el-select
v-model="form.class_id"
placeholder="请选择分类"
clearable
style="width: 100%"
@change="handleCategoryChange"
>
<el-option v-for="item in categoryList" :key="item.class_id" :label="item.name" :value="item.class_id" />
<el-option label="+ 创建新分类" value="" class="create-new-option" />
</el-select>
<div class="new-category-input" v-if="showNewCategoryInput">
<el-input
v-model="form.class_name"
placeholder="输入新分类名称"
>
<template #append>
<el-button @click="createNewCategory">创建</el-button>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item label="版本号" prop="vm_gen">
<el-input v-model="form.vm_gen" placeholder="例如:v1.0.0" />
</el-form-item>
</div>
<el-form-item label="关联套餐" prop="plan_id">
<el-select v-model="form.plan_id" placeholder="请选择适用的套餐" style="width: 100%">
<el-option v-for="item in planList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-card>
</div>
<!-- 右侧图标与高级 -->
<div class="form-side-col">
<el-card class="premium-card" shadow="never">
<div class="section-header small">
<div class="section-info">
<h3>镜像图标</h3>
</div>
</div>
<div class="icon-uploader">
<div class="icon-preview" v-if="form.image_ico">
<img :src="mainUrl + form.image_ico" />
<div class="icon-actions">
<el-button size="small" circle @click="form.image_ico = ''"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="upload-area" v-else>
<div class="upload-placeholder">
<el-icon class="upload-icon"><Picture /></el-icon>
<div class="upload-text">点击上传或选择图标</div>
</div>
<div class="upload-buttons">
<el-button type="primary" size="small" @click="$refs.fileInput.click()">本地上传</el-button>
<el-button size="small" @click="openPicLibrary">素材库</el-button>
</div>
<input ref="fileInput" type="file" style="display: none" @change="onFileSelected" accept="image/*" />
</div>
</div>
</el-card>
</div>
</el-form>
</div>
<!-- 素材库对话框 -->
<el-dialog v-model="picSwitch" title="选择图标" width="800px" append-to-body>
<div class="pic-search">
<el-input
v-model="picPagin.key"
placeholder="搜索图标..."
prefix-icon="Search"
clearable
@change="getpicList"
/>
</div>
<div class="pic-grid" v-loading="picLoading">
<div
v-for="(item, index) in picList"
:key="index"
class="pic-item"
:class="{ active: currentIndex === index }"
@click="selectImage(index)"
>
<img :src="`${mainUrl}/v1/attachment/get_attachment?aid=${item.attachment_id}`" />
<div class="pic-name">{{ item.title || '未命名' }}</div>
<div class="pic-check" v-if="currentIndex === index"><el-icon><Check /></el-icon></div>
</div>
</div>
<div class="pagination-wrapper">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:current-page="picPagin.page"
:page-size="picPagin.count"
@current-change="CurrentPageChange"
/>
</div>
<template #footer>
<el-button @click="picSwitch = false">取消</el-button>
<el-button type="primary" @click="confirmPicSelection" :disabled="currentIndex === null">确定选择</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElNotification } from 'element-plus'
import {
ArrowLeft, Monitor, Folder, SetUp, Picture, Delete,
Search, Check
} from '@element-plus/icons-vue'
import { getServerPlan } from '@/utils/acs/server'
import {
editMirror, addVirtualMirror, getImageTypeList, createImageType, getUserMirrorList
} from '@/utils/acs/mirror'
import { uploadFile, getFileList } from '@/utils/acs/message'
import { mainUrl } from '@/utils/request'
import { useTagsViewStore } from '@/store/tagsViewStore'
const route = useRoute()
const router = useRouter()
const tagsViewStore = useTagsViewStore()
const formRef = ref(null)
const submitting = ref(false)
const goBack = () => {
tagsViewStore.delVisitedView(route)
router.back()
}
const isEdit = computed(() => !!route.query.id)
const serverId = computed(() => route.query.server_id)
const form = reactive({
id: '',
name: '',
show_name: '',
description: '',
server_type: 'hyperV',
plan_id: '',
image_ico: '',
server_id: '',
path: '',
class_id: '',
class_name: '',
vm_gen: ''
})
const rules = {
name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
path: [{ required: true, message: '请输入文件路径', trigger: 'blur' }],
show_name: [{ required: true, message: '请输入展示名称', trigger: 'blur' }]
}
const categoryList = ref([])
const planList = ref([])
const showNewCategoryInput = ref(false)
// 素材库相关
const picSwitch = ref(false)
const picLoading = ref(false)
const picPagin = reactive({
count: 20,
page: 1,
key: '',
user_type: 1
})
const picList = ref([])
const total = ref(0)
const currentIndex = ref(null)
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: '',
name: '',
show_name: '',
description: '',
server_type: 'hyperV',
plan_id: '',
image_ico: '',
server_id: '',
path: '',
class_id: '',
class_name: '',
vm_gen: ''
})
}
// 初始化数据
const initData = async () => {
resetForm()
if (!serverId.value) {
ElMessage.error('缺少服务器ID参数')
return
}
form.server_id = serverId.value
try {
// 获取套餐列表
const planRes = await getServerPlan({ server_id: serverId.value })
if (planRes.data.code === 200) {
planList.value = planRes.data.data.map(item => ({
name: item.name,
id: item.plan_id
}))
}
// 获取分类列表
await fetchCategoryList()
// 如果是编辑模式,填充数据
if (isEdit.value) {
const id = route.query.id
// 尝试从 history.state 获取数据
const stateData = history.state.params ? JSON.parse(JSON.stringify(history.state.params)) : null
if (stateData && stateData.id == id) {
Object.keys(form).forEach(key => {
if (key in stateData) {
form[key] = stateData[key]
}
})
// 确保 ID 类型一致性 (有些时候 API 返回的是数字,有些时候是字符串)
if (form.plan_id) form.plan_id = Number(form.plan_id) || form.plan_id
if (form.class_id) form.class_id = Number(form.class_id) || form.class_id
} else {
// Fallback: fetch list and find item
const listRes = await getUserMirrorList({
server_id: serverId.value,
count: 100,
page: 1
})
if (listRes.data.code === 200) {
const found = listRes.data.data.find(item => item.id == id)
if (found) {
Object.keys(form).forEach(key => {
if (key in found) form[key] = found[key]
})
if (form.plan_id) form.plan_id = Number(form.plan_id) || form.plan_id
if (form.class_id) form.class_id = Number(form.class_id) || form.class_id
}
}
}
}
} catch (error) {
console.error('初始化数据失败:', error)
ElMessage.error('数据加载失败')
}
}
const fetchCategoryList = async () => {
try {
const res = await getImageTypeList(serverId.value)
if (res.data.code === 200) {
categoryList.value = res.data.data || []
}
} catch (error) {
console.error('获取分类失败:', error)
}
}
const handleCategoryChange = (val) => {
if (val === '') {
// 选择了创建新分类
form.class_id = ''
showNewCategoryInput.value = true
} else {
showNewCategoryInput.value = false
form.class_name = ''
}
}
const createNewCategory = async () => {
if (!form.class_name.trim()) {
ElMessage.warning('请输入分类名称')
return
}
try {
const res = await createImageType(serverId.value, form.class_name.trim(), '')
if (res.data.code === 200) {
ElMessage.success('分类创建成功')
await fetchCategoryList()
// 选中新创建的分类
const newCat = categoryList.value.find(c => c.name === form.class_name.trim())
if (newCat) {
form.class_id = newCat.class_id
showNewCategoryInput.value = false
form.class_name = ''
}
} else {
ElMessage.error(res.data.msg || '创建失败')
}
} catch (error) {
ElMessage.error('创建分类失败')
}
}
// 图片上传与选择
const onFileSelected = async (event) => {
const file = event.target.files[0]
if (!file) return
try {
const res = await uploadFile({ file })
if (res.data.code === 200) {
form.image_ico = '/v1/attachment/get_attachment?aid=' + res.data.data.attachment_id
ElMessage.success('上传成功')
} else {
ElMessage.error('上传失败')
}
} catch (error) {
ElMessage.error('上传出错')
}
}
const openPicLibrary = () => {
picSwitch.value = true
getpicList()
}
const getpicList = async () => {
picLoading.value = true
try {
const res = await getFileList(picPagin)
if (res.data.code === 200) {
picList.value = res.data.data
total.value = res.data.count
}
} finally {
picLoading.value = false
}
}
const selectImage = (index) => {
currentIndex.value = index
}
const confirmPicSelection = () => {
if (currentIndex.value !== null) {
const item = picList.value[currentIndex.value]
form.image_ico = `/v1/attachment/get_attachment?aid=${item.attachment_id}`
picSwitch.value = false
}
}
const CurrentPageChange = (page) => {
picPagin.page = page
getpicList()
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true
try {
const submitData = { ...form }
// 处理分类逻辑
if (submitData.class_id) {
submitData.class_name = ''
} else if (submitData.class_name) {
submitData.class_id = ''
} else {
submitData.class_id = ''
submitData.class_name = ''
}
let res
if (isEdit.value) {
submitData.image_id = submitData.id
delete submitData.id
res = await editMirror(submitData)
} else {
res = await addVirtualMirror(submitData)
}
if (res.data.code === 200) {
ElNotification({
title: '操作成功',
message: isEdit.value ? '镜像更新成功' : '镜像创建成功',
type: 'success'
})
goBack()
} else {
ElMessage.error(res.data.msg || '操作失败')
}
} catch (error) {
console.error(error)
ElMessage.error('提交失败')
} finally {
submitting.value = false
}
}
})
}
onMounted(() => {
initData()
})
</script>
<style scoped>
.image-form-container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
/* 顶部导航 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: #ffffff;
padding: 20px 32px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
border: none;
background: #f2f3f5;
color: #606266;
width: 36px;
height: 36px;
transition: all 0.3s;
}
.back-btn:hover {
background-color: #e6e8eb;
color: #303133;
}
.header-title-area {
display: flex;
flex-direction: column;
justify-content: center;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.2;
}
.page-subtitle {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
.submit-btn {
padding: 10px 24px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
/* 表单布局 */
.form-wrapper {
display: flex;
gap: 24px;
align-items: flex-start;
}
.main-form {
display: flex;
width: 100%;
gap: 24px;
}
.form-main-col {
flex: 1;
min-width: 0;
}
.form-side-col {
width: 320px;
flex-shrink: 0;
}
/* 卡片样式 */
.premium-card {
border: 1px solid #e4e7ed;
border-radius: 12px;
background: #ffffff;
transition: all 0.3s;
}
.premium-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.premium-card :deep(.el-card__body) {
padding: 32px;
}
/* 章节标题 */
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f2f5;
}
.section-header.small {
margin-bottom: 20px;
padding-bottom: 12px;
}
.section-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
color: #409EFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.section-info h3 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: #303133;
}
.section-info p {
margin: 2px 0 0 0;
font-size: 12px;
color: #909399;
}
/* 表单网格 */
.form-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* 新分类输入 */
.new-category-input {
margin-top: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border: 1px dashed #dcdfe6;
}
/* 图标上传器 */
.icon-uploader {
width: 100%;
}
.icon-preview {
position: relative;
width: 100%;
height: 160px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
}
.icon-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.icon-actions {
position: absolute;
top: 8px;
right: 8px;
}
.upload-area {
border: 2px dashed #e4e7ed;
border-radius: 8px;
padding: 24px;
text-align: center;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #409EFF;
background: #f2f6fc;
}
.upload-placeholder {
margin-bottom: 16px;
}
.upload-icon {
font-size: 32px;
color: #909399;
margin-bottom: 8px;
}
.upload-text {
font-size: 13px;
color: #606266;
}
.upload-buttons {
display: flex;
justify-content: center;
gap: 12px;
}
/* 素材库网格 */
.pic-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin: 20px 0;
max-height: 400px;
overflow-y: auto;
}
.pic-item {
position: relative;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.pic-item:hover {
border-color: #409EFF;
transform: translateY(-2px);
}
.pic-item.active {
border-color: #409EFF;
background: #ecf5ff;
}
.pic-item img {
width: 48px;
height: 48px;
object-fit: contain;
}
.pic-name {
font-size: 12px;
color: #606266;
text-align: center;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pic-check {
position: absolute;
top: 4px;
right: 4px;
color: #409EFF;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
/* 响应式 */
@media screen and (max-width: 992px) {
.main-form {
flex-direction: column;
}
.form-side-col {
width: 100%;
}
}
@media screen and (max-width: 768px) {
.image-form-container {
padding: 16px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 16px;
}
.header-actions {
width: 100%;
display: flex;
gap: 12px;
}
.header-actions .el-button {
flex: 1;
}
.form-grid-2 {
grid-template-columns: 1fr;
}
.pic-grid {
grid-template-columns: repeat(3, 1fr);
}
}
</style>
+187 -122
View File
@@ -1,103 +1,103 @@
<template> <template>
<div class="image-requests-container"> <div class="image-requests-container">
<div class="page-header"> <el-card class="main-container" shadow="never">
<h2>申请镜像</h2> <!-- 搜索和操作栏 -->
<div class="header-actions"> <div class="filter-section">
<el-button type="primary" @click="handleAdd"> <div class="filter-content">
<el-icon><plus /></el-icon>申请镜像 <el-form :inline="true" :model="searchForm" class="search-form">
</el-button> <el-form-item label="镜像名称">
<el-button @click="handleRefresh"> <el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
<el-icon><refresh /></el-icon>刷新 </el-form-item>
</el-button> <el-form-item label="镜像类型">
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable style="width: 150px">
<el-option label="Docker镜像" value="docker" />
<el-option label="Windows镜像" value="windows" />
</el-select>
</el-form-item>
<el-form-item label="申请状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
<el-option label="已通过" value="approved" />
<el-option label="审核中" value="pending" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<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 @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div> </div>
</div>
<!-- 提示信息 --> <!-- 提示信息 -->
<el-alert <el-alert
type="info" type="info"
show-icon show-icon
:closable="false" :closable="false"
class="info-alert" class="info-alert"
> style="margin: 20px 20px 0; width: auto;"
<el-icon><info-filled /></el-icon>
申请的镜像需要经过安全审核审核通过后可在创建云电脑或容器时使用审核结果将通过站内信通知
</el-alert>
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable />
</el-form-item>
<el-form-item label="镜像类型">
<el-select v-model="searchForm.type" placeholder="请选择镜像类型" clearable>
<el-option label="Docker镜像" value="docker" />
<el-option label="Windows镜像" value="windows" />
</el-select>
</el-form-item>
<el-form-item label="申请状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="已通过" value="approved" />
<el-option label="审核中" value="pending" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="id"
> >
<el-table-column prop="id" label="申请ID" width="150" align="center" /> <template #title>
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip /> 申请的镜像需要经过安全审核审核通过后可在创建云电脑或容器时使用审核结果将通过站内信通知
<el-table-column prop="type" label="类型" width="120" align="center"> </template>
<template #default="scope"> </el-alert>
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看详情
</el-button>
<el-button
v-if="scope.row.status === 'rejected'"
type="primary"
link
@click="handleResubmit(scope.row)"
>
<el-icon><refresh /></el-icon>重新提交
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 --> <!-- 数据表格 -->
<div class="pagination-container"> <div class="table-section">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="申请ID" width="150" align="center" />
<el-table-column prop="name" label="镜像名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="type" label="类型" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.type === 'docker' ? 'success' : 'primary'">
{{ scope.row.type === 'docker' ? 'Docker' : 'Windows' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="requestTime" label="申请时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看详情
</el-button>
<el-button
v-if="scope.row.status === 'rejected'"
type="primary"
link
@click="handleResubmit(scope.row)"
>
<el-icon><refresh /></el-icon>重新提交
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <el-pagination
v-model:current-page="pagination.currentPage" v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize" v-model:page-size="pagination.pageSize"
@@ -106,6 +106,8 @@
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
background
class="pagination"
/> />
</div> </div>
</el-card> </el-card>
@@ -589,41 +591,58 @@ onMounted(() => {
<style scoped> <style scoped>
.image-requests-container { .image-requests-container {
padding: 20px; padding: 0;
} }
.page-header { .main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; padding: 16px 20px;
} gap: 20px;
.header-actions {
display: flex;
gap: 10px;
}
.info-alert {
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.table-card { .search-form {
margin-bottom: 20px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
} }
.pagination-container { .search-form :deep(.el-form-item) {
margin-top: 20px; margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex; display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -637,12 +656,16 @@ onMounted(() => {
/* 环境变量配置样式 */ /* 环境变量配置样式 */
.env-vars-container { .env-vars-container {
margin-bottom: 20px; margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
} }
.env-vars-header { .env-vars-header {
display: flex; display: flex;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: bold; font-weight: 600;
color: #606266;
} }
.env-vars-item { .env-vars-item {
@@ -665,17 +688,23 @@ onMounted(() => {
.add-env-btn { .add-env-btn {
margin-top: 10px; margin-top: 10px;
width: 100%;
border-style: dashed;
} }
/* 端口配置样式 */ /* 端口配置样式 */
.ports-container { .ports-container {
margin-bottom: 20px; margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
} }
.ports-header { .ports-header {
display: flex; display: flex;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: bold; font-weight: 600;
color: #606266;
} }
.ports-item { .ports-item {
@@ -702,6 +731,8 @@ onMounted(() => {
.add-port-btn { .add-port-btn {
margin-top: 10px; margin-top: 10px;
width: 100%;
border-style: dashed;
} }
/* 详情样式 */ /* 详情样式 */
@@ -710,11 +741,13 @@ onMounted(() => {
} }
.request-reason { .request-reason {
background-color: #f8f8f8; background-color: #f8f9fa;
padding: 15px; padding: 15px;
border-radius: 4px; border-radius: 4px;
margin-top: 10px; margin-top: 10px;
white-space: pre-wrap; white-space: pre-wrap;
color: #606266;
line-height: 1.6;
} }
.review-comment { .review-comment {
@@ -724,5 +757,37 @@ onMounted(() => {
margin-top: 10px; margin-top: 10px;
white-space: pre-wrap; white-space: pre-wrap;
border-left: 4px solid #67c23a; border-left: 4px solid #67c23a;
color: #606266;
} }
</style>
/* 表格样式优化 */
: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>
File diff suppressed because it is too large Load Diff
+161 -108
View File
@@ -1,99 +1,93 @@
<template> <template>
<div class="announcements-container"> <div class="announcements-container">
<div class="page-header"> <!-- 主容器 -->
<h2>官方公告</h2> <el-card class="main-container" shadow="never">
<el-button type="primary" @click="handleAdd"> <!-- 搜索和操作栏 -->
<el-icon><plus /></el-icon>发布公告 <div class="filter-section">
</el-button> <div class="filter-content">
</div> <el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="公告标题">
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
<el-option label="已发布" value="published" />
<el-option label="草稿" value="draft" />
<el-option label="已下线" value="offline" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<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>
</div>
</div>
</div>
<!-- 搜索区域 --> <!-- 数据表格 -->
<el-card class="search-card"> <div class="table-section">
<el-form :inline="true" :model="searchForm" class="search-form"> <el-table
<el-form-item label="公告标题"> v-loading="loading"
<el-input v-model="searchForm.title" placeholder="请输入公告标题" clearable /> :data="tableData"
</el-form-item> style="width: 100%"
<el-form-item label="发布时间"> row-key="id"
<el-date-picker :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
v-model="searchForm.dateRange" >
type="daterange" <el-table-column type="index" label="序号" width="60" align="center" />
range-separator="" <el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
start-placeholder="开始日期" <el-table-column prop="publisher" label="发布人" width="120" />
end-placeholder="结束日期" <el-table-column prop="publishTime" label="发布时间" width="180" />
format="YYYY-MM-DD" <el-table-column prop="viewCount" label="查看数" width="100" align="center" />
value-format="YYYY-MM-DD" <el-table-column prop="status" label="状态" width="100" align="center">
clearable <template #default="scope">
/> <el-tag :type="getStatusType(scope.row.status)">
</el-form-item> {{ getStatusText(scope.row.status) }}
<el-form-item label="状态"> </el-tag>
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable> </template>
<el-option label="已发布" value="published" /> </el-table-column>
<el-option label="草稿" value="draft" /> <el-table-column label="操作" width="220" fixed="right">
<el-option label="已下线" value="offline" /> <template #default="scope">
</el-select> <el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
</el-form-item> <el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-form-item> <el-button
<el-button type="primary" @click="handleSearch"> type="warning"
<el-icon><search /></el-icon>搜索 link
</el-button> @click="handleChangeStatus(scope.row)"
<el-button @click="resetSearch"> v-if="scope.row.status !== 'offline'"
<el-icon><refresh /></el-icon>重置 >下线</el-button>
</el-button> <el-button
</el-form-item> type="danger"
</el-form> link
</el-card> @click="handleDelete(scope.row)"
v-if="scope.row.status === 'offline'"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据表格 --> <!-- 分页 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="id"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
<el-table-column prop="viewCount" label="查看数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看
</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="primary"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status !== 'offline'"
>
<el-icon><turn-off /></el-icon>下线
</el-button>
<el-button
type="primary"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'offline'"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination <el-pagination
v-model:current-page="pagination.currentPage" v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize" v-model:page-size="pagination.pageSize"
@@ -102,6 +96,8 @@
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
background
class="pagination"
/> />
</div> </div>
</el-card> </el-card>
@@ -365,32 +361,58 @@ onMounted(() => {
<style scoped> <style scoped>
.announcements-container { .announcements-container {
padding: 20px; padding: 0;
} }
.page-header { .main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; padding: 16px 20px;
} gap: 20px;
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.table-card { .search-form {
margin-bottom: 20px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
} }
.pagination-container { .search-form :deep(.el-form-item) {
margin-top: 20px; margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex; display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -420,4 +442,35 @@ onMounted(() => {
line-height: 1.8; line-height: 1.8;
color: #606266; color: #606266;
} }
</style>
/* 表格样式优化 */
: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>
+142 -92
View File
@@ -1,58 +1,61 @@
<template> <template>
<div class="news-container"> <div class="news-container">
<div class="page-header"> <!-- 主容器 -->
<h2>新闻咨询</h2> <el-card class="main-container" shadow="never">
<el-button type="primary" @click="handleAdd"> <!-- 搜索和操作栏 -->
<el-icon><plus /></el-icon>发布新闻 <div class="filter-section">
</el-button> <div class="filter-content">
</div> <el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="新闻标题">
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="新闻分类">
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable style="width: 150px">
<el-option label="产品动态" value="product" />
<el-option label="技术干货" value="technology" />
<el-option label="行业资讯" value="industry" />
<el-option label="活动公告" value="activity" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<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>
</div>
</div>
</div>
<!-- 搜索区域 --> <!-- 数据表格 -->
<el-card class="search-card"> <div class="table-section">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="新闻标题">
<el-input v-model="searchForm.title" placeholder="请输入新闻标题" clearable />
</el-form-item>
<el-form-item label="新闻分类">
<el-select v-model="searchForm.category" placeholder="请选择分类" clearable>
<el-option label="产品动态" value="product" />
<el-option label="技术干货" value="technology" />
<el-option label="行业资讯" value="industry" />
<el-option label="活动公告" value="activity" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<el-icon><refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 新闻列表卡片 -->
<div v-loading="loading" class="news-list">
<el-card class="table-card">
<el-table <el-table
v-loading="loading"
:data="newsData" :data="newsData"
border
style="width: 100%" style="width: 100%"
row-key="id" row-key="id"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column type="index" label="序号" width="60" align="center" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip /> <el-table-column prop="title" label="新闻标题" min-width="200" show-overflow-tooltip />
@@ -68,33 +71,27 @@
<el-table-column prop="viewCount" label="阅读量" width="100" align="center" /> <el-table-column prop="viewCount" label="阅读量" width="100" align="center" />
<el-table-column label="操作" width="220" align="center" fixed="right"> <el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)"> <el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-icon><view /></el-icon>查看 <el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
</el-button> <el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">
<el-icon><delete /></el-icon>删除
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-container"> <el-pagination
<el-pagination v-model:current-page="pagination.currentPage"
v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
v-model:page-size="pagination.pageSize" :page-sizes="[10, 20, 50, 100]"
:page-sizes="[10, 20, 50, 100]" :total="pagination.total"
:total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@size-change="handleSizeChange" @current-change="handleCurrentChange"
@current-change="handleCurrentChange" background
/> class="pagination"
</div> />
</el-card> </div>
</div> </el-card>
<!-- 新闻详情对话框 --> <!-- 新闻详情对话框 -->
<el-dialog <el-dialog
@@ -400,36 +397,58 @@ onMounted(() => {
<style scoped> <style scoped>
.news-container { .news-container {
padding: 20px; padding: 0;
} }
.page-header { .main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; padding: 16px 20px;
} gap: 20px;
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.news-list { .search-form {
margin-bottom: 20px; margin: 0;
} flex: 1;
.table-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex; display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -487,4 +506,35 @@ onMounted(() => {
margin-right: 5px; margin-right: 5px;
color: #E6A23C; color: #E6A23C;
} }
</style>
/* 表格样式优化 */
: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>
+170 -117
View File
@@ -1,108 +1,102 @@
<template> <template>
<div class="policies-container"> <div class="policies-container">
<div class="page-header"> <!-- 主容器 -->
<h2>官方政策</h2> <el-card class="main-container" shadow="never">
<el-button type="primary" @click="handleAdd"> <!-- 搜索和操作栏 -->
<el-icon><plus /></el-icon>发布政策 <div class="filter-section">
</el-button> <div class="filter-content">
</div> <el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="政策标题">
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="政策类型">
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable style="width: 150px">
<el-option label="服务条款" value="terms" />
<el-option label="定价政策" value="pricing" />
<el-option label="隐私政策" value="privacy" />
<el-option label="合规政策" value="compliance" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="发布时间">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetSearch">
<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>
</div>
</div>
</div>
<!-- 搜索区域 --> <!-- 数据表格 -->
<el-card class="search-card"> <div class="table-section">
<el-form :inline="true" :model="searchForm" class="search-form"> <el-table
<el-form-item label="政策标题"> v-loading="loading"
<el-input v-model="searchForm.title" placeholder="请输入政策标题" clearable /> :data="tableData"
</el-form-item> style="width: 100%"
<el-form-item label="政策类型"> row-key="id"
<el-select v-model="searchForm.type" placeholder="请选择政策类型" clearable> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-option label="服务条款" value="terms" /> >
<el-option label="定价政策" value="pricing" /> <el-table-column type="index" label="序号" width="60" align="center" />
<el-option label="隐私政策" value="privacy" /> <el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
<el-option label="合规政策" value="compliance" /> <el-table-column prop="type" label="政策类型" width="120" align="center">
<el-option label="其他" value="other" /> <template #default="scope">
</el-select> <el-tag :type="getPolicyTypeTag(scope.row.type)">
</el-form-item> {{ getPolicyTypeText(scope.row.type) }}
<el-form-item label="发布时间"> </el-tag>
<el-date-picker </template>
v-model="searchForm.dateRange" </el-table-column>
type="daterange" <el-table-column prop="publisher" label="发布人" width="120" align="center" />
range-separator="" <el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
start-placeholder="开始日期" <el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
end-placeholder="结束日期" <el-table-column prop="status" label="状态" width="100" align="center">
format="YYYY-MM-DD" <template #default="scope">
value-format="YYYY-MM-DD" <el-tag :type="getStatusType(scope.row.status)">
clearable {{ getStatusText(scope.row.status) }}
/> </el-tag>
</el-form-item> </template>
<el-form-item> </el-table-column>
<el-button type="primary" @click="handleSearch"> <el-table-column label="操作" width="220" align="center" fixed="right">
<el-icon><search /></el-icon>搜索 <template #default="scope">
</el-button> <el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-button @click="resetSearch"> <el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-icon><refresh /></el-icon>重置 <el-button
</el-button> type="warning"
</el-form-item> link
</el-form> @click="handleChangeStatus(scope.row)"
</el-card> v-if="scope.row.status === 'active'"
>下线</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'inactive'"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 数据表格 --> <!-- 分页 -->
<el-card class="table-card">
<el-table
v-loading="loading"
:data="tableData"
border
style="width: 100%"
row-key="id"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="title" label="政策标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="type" label="政策类型" width="120" align="center">
<template #default="scope">
<el-tag :type="getPolicyTypeTag(scope.row.type)">
{{ getPolicyTypeText(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="publisher" label="发布人" width="120" align="center" />
<el-table-column prop="publishTime" label="发布时间" width="180" align="center" />
<el-table-column prop="effectiveTime" label="生效时间" width="180" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">
<el-icon><view /></el-icon>查看
</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">
<el-icon><edit /></el-icon>编辑
</el-button>
<el-button
type="primary"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status === 'active'"
>
<el-icon><turn-off /></el-icon>下线
</el-button>
<el-button
type="primary"
link
@click="handleDelete(scope.row)"
v-if="scope.row.status === 'inactive'"
>
<el-icon><delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination <el-pagination
v-model:current-page="pagination.currentPage" v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize" v-model:page-size="pagination.pageSize"
@@ -111,6 +105,8 @@
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
background
class="pagination"
/> />
</div> </div>
</el-card> </el-card>
@@ -435,32 +431,58 @@ onMounted(() => {
<style scoped> <style scoped>
.policies-container { .policies-container {
padding: 20px; padding: 0;
} }
.page-header { .main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; padding: 16px 20px;
} gap: 20px;
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.table-card { .search-form {
margin-bottom: 20px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
} }
.pagination-container { .search-form :deep(.el-form-item) {
margin-top: 20px; margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex; display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -495,4 +517,35 @@ onMounted(() => {
line-height: 1.8; line-height: 1.8;
color: #606266; color: #606266;
} }
</style>
/* 表格样式优化 */
: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>
File diff suppressed because it is too large Load Diff
+847
View File
@@ -0,0 +1,847 @@
<template>
<div class="server-form-container">
<!-- 顶部导航 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" class="back-btn" circle>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<div class="header-title-area">
<h1 class="page-title">{{ isEdit ? '编辑服务器' : '新建服务器' }}</h1>
<span class="page-subtitle">{{ isEdit ? '修改服务器配置信息' : '配置并部署新的服务器节点' }}</span>
</div>
</div>
<div class="header-actions">
<el-button @click="goBack" size="large">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitting" size="large" class="submit-btn">
{{ isEdit ? '保存修改' : '立即创建' }}
</el-button>
</div>
</div>
<!-- 主表单区域 -->
<div class="form-wrapper">
<el-form :model="form" label-position="top" :rules="rules" ref="formRef" class="main-form" size="large">
<!-- 左侧主要配置 -->
<div class="form-main-col">
<el-card class="premium-card" shadow="never">
<div class="section-header">
<div class="section-icon"><el-icon><Monitor /></el-icon></div>
<div class="section-info">
<h3>基础信息</h3>
<p>配置服务器的基本标识与网络信息</p>
</div>
</div>
<div class="form-grid-2">
<el-form-item label="服务器名称" prop="name">
<el-input v-model="form.name" placeholder="例如:生产环境-Web节点-01" />
</el-form-item>
<el-form-item label="IP地址" prop="server_ip">
<el-input v-model="form.server_ip" placeholder="例如:192.168.1.100" />
</el-form-item>
</div>
<el-form-item label="所在地区" prop="location">
<el-cascader
v-model="locationArray"
:options="regionsBuff"
:props="optionProps"
placeholder="选择服务器所在的物理位置"
style="width: 100%"
clearable
/>
</el-form-item>
<el-divider />
<div class="section-header">
<div class="section-icon"><el-icon><Cpu /></el-icon></div>
<div class="section-info">
<h3>硬件规格</h3>
<p>定义服务器的计算资源配额</p>
</div>
</div>
<div class="resource-cards">
<div class="resource-item">
<div class="resource-label">CPU核心</div>
<el-input v-model="form.cpu" placeholder="0">
<template #suffix></template>
</el-input>
</div>
<div class="resource-item">
<div class="resource-label">内存容量</div>
<el-input v-model="form.memory" placeholder="0">
<template #suffix>MB</template>
</el-input>
</div>
<div class="resource-item">
<div class="resource-label">硬盘空间</div>
<el-input v-model="form.disk" placeholder="0">
<template #suffix>GB</template>
</el-input>
</div>
<div class="resource-item">
<div class="resource-label">网络带宽</div>
<el-input v-model="form.bandwidth" placeholder="0">
<template #suffix>Mbps</template>
</el-input>
</div>
</div>
</el-card>
<el-card class="premium-card" shadow="never" style="margin-top: 24px;">
<div class="section-header">
<div class="section-icon"><el-icon><Connection /></el-icon></div>
<div class="section-info">
<h3>连接与认证</h3>
<p>配置服务器的访问方式与凭证</p>
</div>
</div>
<el-form-item label="服务器类型" prop="server_type" style="margin-bottom: 24px;">
<div class="type-selector">
<div
class="type-card"
:class="{ active: form.server_type === 'dockerContainer' }"
@click="form.server_type = 'dockerContainer'"
>
<div class="type-icon"><el-icon><Box /></el-icon></div>
<div class="type-info">
<div class="type-name">容器云服务器</div>
<div class="type-desc">基于Docker容器技术的轻量级实例</div>
</div>
<div class="type-check" v-if="form.server_type === 'dockerContainer'">
<el-icon><Check /></el-icon>
</div>
</div>
<div
class="type-card"
:class="{ active: form.server_type === 'hyperV' }"
@click="form.server_type = 'hyperV'"
>
<div class="type-icon"><el-icon><Platform /></el-icon></div>
<div class="type-info">
<div class="type-name">虚拟机云服务器</div>
<div class="type-desc">基于Hyper-V技术的完整虚拟化实例</div>
</div>
<div class="type-check" v-if="form.server_type === 'hyperV'">
<el-icon><Check /></el-icon>
</div>
</div>
</div>
</el-form-item>
<!-- 容器云特有 -->
<template v-if="form.server_type === 'dockerContainer'">
<el-form-item label="Auth-ID" prop="auth_id">
<el-input v-model="form.auth_id" placeholder="输入服务器管理ID" />
</el-form-item>
</template>
<!-- 虚拟机特有 -->
<template v-if="form.server_type === 'hyperV'">
<el-form-item label="Guacamole网关" prop="guacamole_id">
<el-select
v-model="form.guacamole_id"
placeholder="选择Guacamole连接配置"
filterable
clearable
:loading="guacamoleLoading"
@change="handleGuacamoleChange"
style="width: 100%"
>
<el-option
v-for="item in guacamoleList"
:key="item.id"
:label="item.url"
:value="item.id"
>
<div class="guacamole-option">
<span class="url">{{ item.url }}</span>
<span class="user">{{ item.username }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<div class="form-grid-2">
<el-form-item label="登录用户名" prop="username">
<el-input v-model="form.username" placeholder="例如:Administrator" />
</el-form-item>
<el-form-item label="登录密码" prop="password">
<el-input
v-model="form.password"
placeholder="输入登录密码"
type="password"
show-password
/>
</el-form-item>
</div>
<el-form-item>
<div class="feature-switch">
<div class="switch-info">
<span class="switch-title">端口映射</span>
<span class="switch-desc">允许外部网络访问该服务器的特定端口</span>
</div>
<el-switch
v-model="form.allow_port_forward"
:active-value="1"
:inactive-value="0"
/>
</div>
</el-form-item>
</template>
<el-form-item label="管理Token" prop="server_token">
<el-input
v-model="form.server_token"
placeholder="节点服务器管理员Token"
type="password"
show-password
/>
</el-form-item>
</el-card>
</div>
<!-- 右侧高级设置 -->
<div class="form-side-col">
<el-card class="premium-card" shadow="never">
<div class="section-header small">
<div class="section-info">
<h3>高级设置</h3>
</div>
</div>
<el-form-item label="控制台连接">
<el-input v-model="form.console_url" placeholder="可选,https需反代">
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="展示卡片HTML">
<el-input
v-model="form.html"
type="textarea"
:rows="6"
placeholder="自定义购买页面的展示样式代码"
resize="none"
/>
</el-form-item>
<el-divider />
<el-form-item>
<div class="feature-switch">
<div class="switch-info">
<span class="switch-title">购物车显示</span>
<span class="switch-desc">在前端购买页面展示此节点</span>
</div>
<el-switch
v-model="form.hide"
:active-value="0"
:inactive-value="1"
/>
</div>
</el-form-item>
</el-card>
</div>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElNotification } from 'element-plus'
import {
ArrowLeft, Monitor, Cpu, Link, Connection,
Box, Platform, Check
} from '@element-plus/icons-vue'
import { addServer, editServer, getServer } from '@/utils/acs/server'
import { getGuacamoleList } from '@/utils/acs/guacamole'
import regions from '@/utils/regions.json'
const route = useRoute()
const router = useRouter()
const formRef = ref(null)
const submitting = ref(false)
const isEdit = computed(() => !!route.query.id)
const form = reactive({
server_id: '',
name: '',
server_ip: '',
location: '',
bandwidth: '',
disk: '',
memory: '',
cpu: '',
state: '',
auth_id: '',
server_token: '',
server_type: 'dockerContainer',
html: '',
hide: 0,
console_url: '',
guacamole_id: '',
username: '',
password: '',
allow_port_forward: 0
})
const rules = {
name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
server_ip: [
{ required: true, message: '请输入IP地址', trigger: 'blur' },
{ pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: '请输入有效的IP地址', trigger: 'blur' }
],
guacamole_id: [
{ required: false, message: '请输入Guacamole服务ID', trigger: 'blur' }
],
username: [
{ required: false, message: '请输入登录用户名', trigger: 'blur' }
],
password: [
{ required: false, message: '请输入登录密码', trigger: 'blur' }
]
}
// Guacamole 相关
const guacamoleList = ref([])
const guacamoleLoading = ref(false)
const fetchGuacamoleList = async () => {
if (guacamoleLoading.value) return
guacamoleLoading.value = true
try {
const res = await getGuacamoleList()
if (res && res.data && res.data.code === 200) {
guacamoleList.value = res.data.data || []
} else {
guacamoleList.value = []
}
} catch (error) {
console.error('获取Guacamole列表失败:', error)
guacamoleList.value = []
} finally {
guacamoleLoading.value = false
}
}
const handleGuacamoleChange = (selectedId) => {
if (!selectedId) {
form.username = ''
form.password = ''
return
}
}
// 地区数据处理
const regionsBuff = ref(regions)
const optionProps = {
label: 'label',
value: 'value',
children: 'children',
checkStrictly: false,
emitPath: true
}
const findValueByLabel = (label, options) => {
for (const option of options) {
if (option.label === label) return option.value
if (option.children) {
const result = findValueByLabel(label, option.children)
if (result) return result
}
}
return undefined
}
const findLabelByValue = (value, options) => {
for (const option of options) {
if (option.value === value) return option.label
if (option.children) {
const result = findLabelByValue(value, option.children)
if (result) return result
}
}
return undefined
}
const locationArray = computed({
get: () => {
if (form.location) {
try {
const labels = form.location.split(' ')
const values = labels.map(label => findValueByLabel(label, regionsBuff.value))
return values.filter(value => value !== undefined)
} catch (error) {
return []
}
}
return []
},
set: (newArray) => {
try {
if (Array.isArray(newArray) && newArray.length > 0) {
const labels = newArray.map(value => {
const label = findLabelByValue(value, regionsBuff.value)
return label || value
})
form.location = labels.join(' ')
} else {
form.location = ''
}
} catch (error) {
form.location = ''
}
}
})
// 初始化数据
const initData = async () => {
if (isEdit.value) {
const id = route.query.id
if (id) {
try {
const stateData = history.state.params
if (stateData) {
Object.keys(form).forEach(key => {
if (key in stateData) {
form[key] = stateData[key]
}
})
} else {
const res = await getServer(1, 100, '', route.query.type || 'dockerContainer')
if (res && res.data && res.data.data) {
const found = res.data.data.find(item => item.server_id == id)
if (found) {
Object.keys(form).forEach(key => {
if (key in found) {
form[key] = found[key]
}
})
}
}
}
} catch (e) {
console.error(e)
}
}
} else {
form.server_type = route.query.type || 'dockerContainer'
}
if (form.server_type === 'hyperV') {
fetchGuacamoleList()
}
}
const goBack = () => {
router.back()
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitting.value = true
try {
const formData = { ...form }
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide', 'allow_port_forward']
numericFields.forEach(field => {
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
formData[field] = Number(formData[field])
}
})
let res
if (!isEdit.value) {
res = await addServer(formData)
} else {
res = await editServer(formData)
}
if (res && res.data && res.data.code === 200) {
ElNotification({
title: !isEdit.value ? '添加成功' : '更新成功',
message: `服务器"${formData.name}"已${!isEdit.value ? '添加' : '更新'}成功`,
type: 'success',
duration: 3000
})
goBack()
} else {
ElMessage.error(res?.data?.msg || '操作失败')
}
} catch (error) {
console.error('提交表单失败:', error)
ElMessage.error('提交失败')
} finally {
submitting.value = false
}
}
})
}
watch(() => form.server_type, (val) => {
if (val === 'hyperV') {
fetchGuacamoleList()
}
})
onMounted(() => {
initData()
})
</script>
<style scoped>
.server-form-container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
/* 顶部导航 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: #ffffff;
padding: 20px 32px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
border: none;
background: #f2f3f5;
color: #606266;
width: 36px;
height: 36px;
transition: all 0.3s;
}
.back-btn:hover {
background-color: #e6e8eb;
color: #303133;
}
.header-title-area {
display: flex;
flex-direction: column;
justify-content: center;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
line-height: 1.2;
}
.page-subtitle {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
.submit-btn {
padding: 10px 24px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
/* 表单布局 */
.form-wrapper {
display: flex;
gap: 24px;
align-items: flex-start;
}
.main-form {
display: flex;
width: 100%;
gap: 24px;
}
.form-main-col {
flex: 1;
min-width: 0;
}
.form-side-col {
width: 320px;
flex-shrink: 0;
}
/* 卡片样式 */
.premium-card {
border: 1px solid #e4e7ed;
border-radius: 12px;
background: #ffffff;
transition: all 0.3s;
}
.premium-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.premium-card :deep(.el-card__body) {
padding: 32px;
}
/* 章节标题 */
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f2f5;
}
.section-header.small {
margin-bottom: 20px;
padding-bottom: 12px;
}
.section-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
color: #409EFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.section-info h3 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: #303133;
}
.section-info p {
margin: 2px 0 0 0;
font-size: 12px;
color: #909399;
}
/* 表单网格 */
.form-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* 资源卡片 */
.resource-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.resource-item {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
border: 1px solid #ebeef5;
transition: all 0.3s;
}
.resource-item:hover {
border-color: #c6e2ff;
background: #f2f6fc;
}
.resource-label {
font-size: 12px;
color: #606266;
margin-bottom: 8px;
font-weight: 500;
}
/* 类型选择器 */
.type-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.type-card {
position: relative;
border: 1px solid #dcdfe6;
border-radius: 8px;
padding: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
background: #fff;
}
.type-card:hover {
border-color: #409EFF;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.type-card.active {
border-color: #409EFF;
background: #ecf5ff;
}
.type-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: #f2f6fc;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #606266;
}
.type-card.active .type-icon {
background: #fff;
color: #409EFF;
}
.type-info {
flex: 1;
}
.type-name {
font-weight: 600;
color: #303133;
font-size: 14px;
margin-bottom: 2px;
}
.type-desc {
font-size: 12px;
color: #909399;
}
.type-check {
position: absolute;
top: 8px;
right: 8px;
color: #409EFF;
font-size: 16px;
}
/* 开关样式 */
.feature-switch {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #ebeef5;
}
.switch-info {
display: flex;
flex-direction: column;
}
.switch-title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.switch-desc {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.guacamole-option {
display: flex;
justify-content: space-between;
width: 100%;
}
.guacamole-option .url {
font-weight: 500;
}
.guacamole-option .user {
color: #909399;
font-size: 12px;
}
/* 响应式 */
@media screen and (max-width: 992px) {
.main-form {
flex-direction: column;
}
.form-side-col {
width: 100%;
}
.resource-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 768px) {
.server-form-container {
padding: 16px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 16px;
}
.header-actions {
width: 100%;
display: flex;
gap: 12px;
}
.header-actions .el-button {
flex: 1;
}
.form-grid-2 {
grid-template-columns: 1fr;
}
.type-selector {
grid-template-columns: 1fr;
}
}
</style>
+129 -196
View File
@@ -446,8 +446,8 @@
<!-- <serverChart v-if="TypeData" :Type="TypeData" class="chart-section" /> --> <!-- <serverChart v-if="TypeData" :Type="TypeData" class="chart-section" /> -->
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<div class="content-wrapper"> <el-card class="main-container" shadow="never">
<el-tabs type="border-card" class="main-tabs"> <el-tabs class="main-tabs">
<!-- 实例规格列表 --> <!-- 实例规格列表 -->
<el-tab-pane label="实例规格列表"> <el-tab-pane label="实例规格列表">
<div class="tab-header"> <div class="tab-header">
@@ -464,11 +464,11 @@
<el-table <el-table
v-loading="specLoading" v-loading="specLoading"
:data="spec_list" :data="spec_list"
border
stripe stripe
style="width: 100%" style="width: 100%"
table-layout="auto" table-layout="auto"
class="data-table" class="data-table"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column prop="plan_id" label="规格ID" width="80" /> <el-table-column prop="plan_id" label="规格ID" width="80" />
<el-table-column prop="name" label="规格名称" min-width="120" show-overflow-tooltip /> <el-table-column prop="name" label="规格名称" min-width="120" show-overflow-tooltip />
@@ -510,24 +510,24 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip /> <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="160" fixed="right"> <el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<div class="table-actions"> <div class="table-actions">
<el-tooltip content="编辑" placement="top" :hide-after="1500"> <el-tooltip content="编辑" placement="top" :hide-after="1500">
<el-button <el-button
type="primary" type="primary"
circle link
:icon="Edit" :icon="Edit"
@click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;" @click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;"
/> >编辑</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="删除" placement="top" :hide-after="1500"> <el-tooltip content="删除" placement="top" :hide-after="1500">
<el-button <el-button
type="danger" type="danger"
circle link
:icon="Delete" :icon="Delete"
@click="deleteSpec(scope.row.plan_id)" @click="deleteSpec(scope.row.plan_id)"
/> >删除</el-button>
</el-tooltip> </el-tooltip>
</div> </div>
</template> </template>
@@ -562,9 +562,9 @@
<el-table <el-table
:data="user_servers" :data="user_servers"
stripe stripe
border
style="width: 100%" style="width: 100%"
class="data-table" class="data-table"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column label="ID" prop="container_id" width="80" /> <el-table-column label="ID" prop="container_id" width="80" />
<el-table-column label="价格" prop="pay" width="100"> <el-table-column label="价格" prop="pay" width="100">
@@ -598,7 +598,7 @@
: scope.row.container_state == 4 : scope.row.container_state == 4
? 'danger' ? 'danger'
: 'info'" : 'info'"
effect="light" effect="plain"
> >
{{ {{
scope.row.container_state == 0 scope.row.container_state == 0
@@ -616,11 +616,11 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100" fixed="right"> <el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-button
type="primary" type="primary"
size="small" link
:icon="Setting" :icon="Setting"
@click="$router.push('/servers/container?container_id=' + scope.row.container_id)" @click="$router.push('/servers/container?container_id=' + scope.row.container_id)"
> >
@@ -667,9 +667,9 @@
<el-table <el-table
:data="floatList" :data="floatList"
style="width: 100%" style="width: 100%"
border
stripe stripe
class="data-table" class="data-table"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column label="ID" prop="id" width="80" /> <el-table-column label="ID" prop="id" width="80" />
<el-table-column label="创建时间" min-width="160"> <el-table-column label="创建时间" min-width="160">
@@ -680,7 +680,7 @@
<el-table-column label="浮动IP" prop="floating_ip" min-width="150" /> <el-table-column label="浮动IP" prop="floating_ip" min-width="150" />
<el-table-column label="状态" width="100" align="center"> <el-table-column label="状态" width="100" align="center">
<template #default="scope"> <template #default="scope">
<el-tag :type="scope.row.is_used ? 'success' : 'info'" effect="light"> <el-tag :type="scope.row.is_used ? 'success' : 'info'" effect="plain">
{{ scope.row.is_used ? "已绑定" : "未绑定" }} {{ scope.row.is_used ? "已绑定" : "未绑定" }}
</el-tag> </el-tag>
</template> </template>
@@ -690,10 +690,10 @@
<el-tooltip content="删除IP" placement="top" :hide-after="1500"> <el-tooltip content="删除IP" placement="top" :hide-after="1500">
<el-button <el-button
type="danger" type="danger"
circle link
:icon="Delete" :icon="Delete"
@click="delFloating(scope.row.id)" @click="delFloating(scope.row.id)"
/> >删除</el-button>
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
@@ -702,7 +702,7 @@
<el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" /> <el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </el-card>
<!-- 添加/编辑实例规格对话框 --> <!-- 添加/编辑实例规格对话框 -->
<el-dialog <el-dialog
@@ -2617,9 +2617,7 @@ import { ElMessageBox } from 'element-plus';
<style scoped> <style scoped>
.server-container { .server-container {
padding: 20px; padding: 0;
background-color: #f5f7fa;
min-height: calc(100vh - 120px);
} }
/* 页面标题区域 */ /* 页面标题区域 */
@@ -2628,6 +2626,9 @@ import { ElMessageBox } from 'element-plus';
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 24px; margin-bottom: 24px;
padding: 16px 20px;
background: #fff;
border-bottom: 1px solid #e1e8ed;
} }
.page-header .left { .page-header .left {
@@ -2638,7 +2639,7 @@ import { ElMessageBox } from 'element-plus';
.page-header .title { .page-header .title {
margin: 0; margin: 0;
font-size: 24px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
} }
@@ -2647,7 +2648,8 @@ import { ElMessageBox } from 'element-plus';
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 12px; padding: 4px 12px;
border-radius: 4px;
} }
.status-dot { .status-dot {
@@ -2669,13 +2671,13 @@ import { ElMessageBox } from 'element-plus';
/* 返回按钮样式 */ /* 返回按钮样式 */
.back-btn { .back-btn {
font-size: 16px; font-size: 16px;
color: #409EFF; color: #606266;
padding: 8px 0; padding: 0;
margin-right: 16px; margin-right: 8px;
} }
.back-btn:hover { .back-btn:hover {
color: #66b1ff; color: #409EFF;
} }
/* 服务器信息卡片 */ /* 服务器信息卡片 */
@@ -2684,6 +2686,7 @@ import { ElMessageBox } from 'element-plus';
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
padding: 0 20px;
} }
/* 服务器详细信息卡片 */ /* 服务器详细信息卡片 */
@@ -2692,16 +2695,17 @@ import { ElMessageBox } from 'element-plus';
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
padding: 0 20px;
} }
.info-card { .info-card {
background: white; background: white;
border-radius: 8px; border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid #ebeef5; border: 1px solid #e1e8ed;
} }
.info-card:hover { .info-card:hover {
@@ -2710,10 +2714,10 @@ import { ElMessageBox } from 'element-plus';
} }
.card-title { .card-title {
background-color: #f5f7fa; background-color: #fafbfc;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #e1e8ed;
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
display: flex; display: flex;
@@ -2722,7 +2726,7 @@ import { ElMessageBox } from 'element-plus';
} }
.card-title .el-icon { .card-title .el-icon {
font-size: 18px; font-size: 16px;
color: #409EFF; color: #409EFF;
} }
@@ -2745,32 +2749,31 @@ import { ElMessageBox } from 'element-plus';
} }
.info-value { .info-value {
font-size: 15px; font-size: 14px;
color: #303133; color: #303133;
word-break: break-all; word-break: break-all;
} }
.info-value.highlight { .info-value.highlight {
font-weight: 600; font-weight: 600;
font-size: 16px;
color: #409EFF; color: #409EFF;
} }
/* 硬件信息样式 - 符合整体设计风格 */ /* 硬件信息样式 */
.device-count { .device-count {
color: #909399; color: #909399;
font-size: 14px; font-size: 13px;
font-weight: normal; font-weight: normal;
margin-left: 8px; margin-left: 8px;
} }
.hardware-summary { .hardware-summary {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 16px; gap: 12px;
margin-bottom: 20px; margin-bottom: 16px;
padding-bottom: 16px; padding-bottom: 16px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #f0f2f5;
} }
.summary-item { .summary-item {
@@ -2780,21 +2783,21 @@ import { ElMessageBox } from 'element-plus';
.hardware-devices { .hardware-devices {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 12px;
} }
.device-item { .device-item {
padding: 16px; padding: 12px;
background-color: #fafbfc; background-color: #fafbfc;
border-radius: 6px; border-radius: 4px;
border: 1px solid #e4e7ed; border: 1px solid #e1e8ed;
} }
.device-header { .device-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 8px;
} }
.device-title { .device-title {
@@ -2807,57 +2810,27 @@ import { ElMessageBox } from 'element-plus';
.device-title .el-icon { .device-title .el-icon {
margin-right: 6px; margin-right: 6px;
font-size: 16px; font-size: 14px;
} }
.device-details { .device-details {
margin-top: 12px; margin-top: 8px;
} }
.detail-row { .detail-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px; gap: 8px;
margin-bottom: 12px; margin-bottom: 8px;
} }
.usage-progress { .usage-progress {
margin-top: 8px; margin-top: 8px;
} }
/* 使用率状态颜色类 */
.info-value.usage-normal {
color: #67C23A;
font-weight: 600;
}
.info-value.usage-medium {
color: #409EFF;
font-weight: 600;
}
.info-value.usage-warning {
color: #E6A23C;
font-weight: 600;
}
.info-value.usage-critical {
color: #F56C6C;
font-weight: 600;
}
/* 实际硬件划分样式 */
.highlight-item {
padding: 8px;
background-color: #f0f9ff;
border-radius: 4px;
border-left: 3px solid #409EFF;
margin-bottom: 16px;
}
/* 流量信息样式 */ /* 流量信息样式 */
.traffic-section { .traffic-section {
margin-bottom: 20px; margin-bottom: 16px;
} }
.traffic-section:last-child { .traffic-section:last-child {
@@ -2865,62 +2838,38 @@ import { ElMessageBox } from 'element-plus';
} }
.section-title { .section-title {
font-size: 14px; font-size: 13px;
font-weight: 600; font-weight: 600;
margin-bottom: 8px;
margin-bottom: 12px;
padding-bottom: 4px; padding-bottom: 4px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #f0f2f5;
color: #606266;
} }
.traffic-speed-grid, .traffic-speed-grid,
.traffic-total-grid { .traffic-total-grid,
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
}
.traffic-total-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.traffic-distribution { .traffic-distribution {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px; gap: 12px;
} }
.raw-data { .raw-data {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px; gap: 12px;
padding: 12px; padding: 12px;
background-color: #fafbfc; background-color: #fafbfc;
border-radius: 6px; border-radius: 4px;
border: 1px solid #e4e7ed; border: 1px solid #e1e8ed;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
} }
.raw-data .info-value { .raw-data .info-value {
font-size: 13px; font-size: 12px;
color: #606266; color: #606266;
} }
.traffic-note {
margin-top: 8px;
}
/* 旧样式保持兼容 */
.usage-high {
color: #F56C6C;
font-weight: 600;
}
.usage-medium {
color: #E6A23C;
font-weight: 600;
}
/* 错误状态样式 */ /* 错误状态样式 */
.error-status { .error-status {
color: #F56C6C; color: #F56C6C;
@@ -2929,15 +2878,10 @@ import { ElMessageBox } from 'element-plus';
} }
/* 主要内容区域 */ /* 主要内容区域 */
.chart-section { .main-container {
margin-bottom: 24px; margin: 0 20px 20px 20px;
} border: 1px solid #e1e8ed;
background: #ffffff;
.content-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
} }
.tab-header { .tab-header {
@@ -2945,11 +2889,13 @@ import { ElMessageBox } from 'element-plus';
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 16px; margin-bottom: 16px;
padding: 16px 20px;
border-bottom: 1px solid #f0f2f5;
} }
.tab-title { .tab-title {
margin: 0; margin: 0;
font-size: 18px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
} }
@@ -2969,40 +2915,67 @@ import { ElMessageBox } from 'element-plus';
gap: 8px; gap: 8px;
} }
/* 表格样式 */ /* 表格样式优化 */
.data-table { :deep(.el-table) {
margin-bottom: 16px; border: none;
color: #2c3e50;
} }
.table-actions { :deep(.el-table__header) {
display: flex; background: #f8f9fa;
justify-content: center;
gap: 8px;
} }
.resource-value { :deep(.el-table th) {
white-space: nowrap; background: #f8f9fa !important;
} border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
.unit {
color: #909399;
font-size: 12px;
margin-left: 2px;
}
.price-tag {
font-weight: 600; font-weight: 600;
color: #F56C6C; font-size: 13px;
} }
.time-info { :deep(.el-table td) {
font-size: 13px; border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
/* Tabs 样式优化 */
:deep(.el-tabs__header) {
margin: 0;
border-bottom: 1px solid #e1e8ed;
background: #fff;
padding: 0 20px;
}
:deep(.el-tabs__item) {
height: 48px;
line-height: 48px;
font-weight: 500;
color: #606266; color: #606266;
} }
:deep(.el-tabs__item.is-active) {
color: #409EFF;
background: #fff;
font-weight: 600;
}
:deep(.el-tabs__content) {
padding: 0;
}
/* 分页样式 */ /* 分页样式 */
.pagination-container { .pagination-container {
margin-top: 20px; padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -3013,15 +2986,13 @@ import { ElMessageBox } from 'element-plus';
} }
.section-title { .section-title {
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
margin-bottom: 16px; margin-bottom: 16px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5; border-bottom: 1px dashed #e1e8ed;
} }
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -3124,27 +3095,24 @@ import { ElMessageBox } from 'element-plus';
gap: 16px; gap: 16px;
} }
.page-header .left {
flex-wrap: wrap;
}
.back-btn {
margin-right: 8px;
margin-bottom: 8px;
}
.server-info { .server-info {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: 0 16px;
} }
.server-detail-info { .server-detail-info {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: 0 16px;
} }
.info-card.location-info { .info-card.location-info {
grid-column: auto; grid-column: auto;
} }
.main-container {
margin: 0 16px 16px 16px;
}
.tab-header { .tab-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -3159,40 +3127,5 @@ import { ElMessageBox } from 'element-plus';
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
} }
/* 硬件信息响应式 */
.hardware-summary {
grid-template-columns: 1fr;
gap: 12px;
}
.detail-row {
grid-template-columns: 1fr;
gap: 8px;
}
.device-item {
padding: 12px;
}
.device-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
/* 流量信息响应式 */
.traffic-speed-grid,
.traffic-total-grid,
.traffic-distribution {
grid-template-columns: 1fr;
gap: 8px;
}
.raw-data {
grid-template-columns: 1fr;
gap: 8px;
padding: 8px;
}
} }
</style> </style>
+200 -193
View File
@@ -1,17 +1,5 @@
<template> <template>
<div class="all-sites-container"> <div class="all-sites-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="left">
<h2 class="title">所有站点</h2>
<el-tag type="info" effect="plain" class="count-tag"> {{ pagination.total }} 个站点</el-tag>
</div>
<div class="actions">
<el-button type="primary" @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
<!-- <el-button type="success" @click="handleExport" :icon="Download" class="action-btn">导出数据</el-button> -->
</div>
</div>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-panel"> <div class="stats-panel">
<div class="stat-card total-card"> <div class="stat-card total-card">
@@ -44,102 +32,115 @@
</div> </div>
</div> </div>
<!-- 搜索和筛选 --> <el-card class="main-container" shadow="never">
<el-card class="filter-container" shadow="never"> <!-- 搜索和筛选 -->
<el-form :inline="true" :model="queryParams" class="search-form"> <div class="filter-section">
<el-form-item label="搜索容器"> <div class="filter-content">
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable /> <el-form :inline="true" :model="queryParams" class="search-form">
</el-form-item> <el-form-item label="搜索容器">
<el-form-item> <el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button> </el-form-item>
<el-button @click="resetQuery" :icon="Delete">重置</el-button> <el-form-item>
</el-form-item> <el-button type="primary" @click="handleQuery">
</el-form> <el-icon><Search /></el-icon>查询
</el-card> </el-button>
<el-button @click="resetQuery">
<el-icon><Delete /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 站点列表 --> <!-- 站点列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="siteList" :data="siteList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
border :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
stripe >
> <el-table-column type="selection" width="55" />
<el-table-column type="selection" width="55" /> <el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip /> <el-table-column prop="url" label="访问地址" min-width="200" show-overflow-tooltip>
<el-table-column prop="url" label="访问地址" min-width="200" show-overflow-tooltip> <template #default="{ row }">
<template #default="{ row }"> <el-link :href="row.url" target="_blank" type="primary" v-if="row.url">
<el-link :href="row.url" target="_blank" type="primary" v-if="row.url"> {{ row.url }}
{{ row.url }} </el-link>
</el-link> <span v-else class="text-muted">无访问地址</span>
<span v-else class="text-muted">无访问地址</span> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="连接类型" width="120" align="center">
<el-table-column label="连接类型" width="120" align="center"> <template #default="{ row }">
<template #default="{ row }"> <el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small"> {{ getConnectTypeText(row.connect_type) }}
{{ getConnectTypeText(row.connect_type) }} </el-tag>
</el-tag> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="连接状态" width="100" align="center">
<el-table-column label="连接状态" width="100" align="center"> <template #default="{ row }">
<template #default="{ row }"> <el-tag
<el-tag :type="getConnectionStatusType(row.connect)"
:type="getConnectionStatusType(row.connect)" effect="plain"
effect="plain" size="small"
size="small" >
> {{ getConnectionStatusText(row.connect) }}
{{ getConnectionStatusText(row.connect) }} </el-tag>
</el-tag> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="违规状态" width="100" align="center">
<el-table-column label="违规状态" width="100" align="center"> <template #default="{ row }">
<template #default="{ row }"> <el-tag
<el-tag :type="row.is_violation ? 'danger' : 'success'"
:type="row.is_violation ? 'danger' : 'success'" effect="plain"
effect="plain" size="small"
size="small" >
> {{ row.is_violation ? '违规' : '正常' }}
{{ row.is_violation ? '违规' : '正常' }} </el-tag>
</el-tag> </template>
</template> </el-table-column>
</el-table-column> <el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" fixed="right" align="center" width="180">
<el-table-column label="操作" fixed="right" align="center"> <template #default="{ row }">
<template #default="{ row }">
<div class="action-buttons">
<el-tooltip content="重新检查" placement="top"> <el-tooltip content="重新检查" placement="top">
<el-button type="warning" :icon="Refresh" circle size="small" @click="handleRecheck(row)" /> <el-button type="warning" link @click="handleRecheck(row)">
<el-icon><Refresh /></el-icon>检查
</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'"> <el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'">
<el-button type="danger" :icon="Warning" circle size="small" @click="handleMarkViolation(row)" /> <el-button type="danger" link @click="handleMarkViolation(row)">
<el-icon><Warning /></el-icon>违规
</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'"> <el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'">
<el-button type="success" :icon="CircleCheck" circle size="small" @click="handleMarkNormal(row)" /> <el-button type="success" link @click="handleMarkNormal(row)">
<el-icon><CircleCheck /></el-icon>正常
</el-button>
</el-tooltip> </el-tooltip>
</div> </template>
</template> </el-table-column>
</el-table-column> </el-table>
</el-table>
<!-- 分页 -->
<!-- 分页 --> <el-pagination
<el-pagination v-model:current-page="queryParams.pageNum"
v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
layout="total, sizes, prev, pager, next, jumper" :total="pagination.total"
:total="pagination.total" @size-change="handleSizeChange"
@size-change="handleSizeChange" @current-change="handleCurrentChange"
@current-change="handleCurrentChange" background
background class="pagination"
class="pagination" />
/> </div>
</el-card> </el-card>
</div> </div>
</template> </template>
@@ -650,42 +651,7 @@ onMounted(() => {
<style scoped> <style scoped>
.all-sites-container { .all-sites-container {
padding: 20px; padding: 0;
min-height: calc(100vh - 120px);
background-color: #f5f7fa;
}
/* 页面标题样式 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.count-tag {
font-size: 13px;
}
.page-header .actions {
display: flex;
gap: 12px;
align-items: center;
} }
/* 统计卡片 */ /* 统计卡片 */
@@ -698,18 +664,18 @@ onMounted(() => {
.stat-card { .stat-card {
background: white; background: white;
border-radius: 8px; border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 20px; padding: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid #ebeef5; border: 1px solid #e1e8ed;
} }
.stat-card:hover { .stat-card:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.stat-icon { .stat-icon {
@@ -753,88 +719,129 @@ onMounted(() => {
font-weight: 600; font-weight: 600;
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1.1; line-height: 1.1;
color: #303133;
} }
.stat-label { .stat-label {
font-size: 14px; font-size: 14px;
color: #606266; color: #909399;
} }
/* 筛选容器 */ .main-container {
.filter-container { border: 1px solid #e1e8ed;
margin-bottom: 20px; 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 { .search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0; margin-bottom: 0;
margin-right: 12px;
} }
/* 表格容器 */ .action-bar {
.table-container {
margin-bottom: 20px;
}
.action-buttons {
display: flex; display: flex;
justify-content: center; gap: 12px;
gap: 8px; flex-shrink: 0;
}
.table-section {
padding: 0;
} }
/* 分页 */
.pagination { .pagination {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
/* 站点详情 */
.site-detail {
padding: 10px 0;
}
.detail-section {
margin-top: 20px; margin-top: 20px;
} padding: 16px 20px;
border-top: 1px solid #e1e8ed;
.detail-section h4 { background: #fafbfc;
margin-bottom: 12px;
color: #303133;
font-weight: 600;
}
/* 对话框底部 */
.dialog-footer {
display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
/* 表格样式优化 */
: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;
}
/* 响应式设计 */ /* 响应式设计 */
@media screen and (max-width: 1200px) { @media screen and (max-width: 992px) {
.stats-panel { .stats-panel {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.stat-card:last-child {
grid-column: span 2;
}
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.page-header .actions {
width: 100%;
flex-wrap: wrap;
}
.stats-panel { .stats-panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
}
.stat-card:last-child {
/* 自定义样式 */ grid-column: auto;
.text-muted { }
color: #909399;
font-style: italic; .filter-content {
flex-direction: column;
align-items: stretch;
}
.search-form {
width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-start;
}
} }
</style> </style>
+199 -282
View File
@@ -1,158 +1,101 @@
<template> <template>
<div class="violation-sites-container"> <div class="violation-sites-container">
<!-- 页面头部 --> <el-card class="main-container" shadow="never">
<div class="page-header"> <!-- 搜索和筛选 -->
<div class="left"> <div class="filter-section">
<h2 class="title">违规站点</h2> <div class="filter-content">
<el-tag type="danger" effect="plain" class="count-tag"> {{ pagination.total }} 个违规站点</el-tag> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="搜索容器">
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">
<el-icon><Delete /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div> </div>
<div class="actions">
<el-button type="primary" @click="handleRefresh" :icon="Refresh" class="action-btn">刷新</el-button>
<!-- <el-button type="warning" @click="handleBatchProcess" :disabled="!selectedRows.length" :icon="Warning" class="action-btn">
批量处理
</el-button>
<el-button type="success" @click="handleExport" :icon="Download" class="action-btn">导出违规报告</el-button> -->
</div>
</div>
<!-- 违规统计卡片 --> <!-- 违规站点列表 -->
<!-- <div class="stats-panel"> <div class="table-section">
<div class="stat-card total-card"> <el-table
<div class="stat-icon"><el-icon><Warning /></el-icon></div> v-loading="loading"
<div class="stat-content"> :data="violationList"
<div class="stat-value">{{ violationStats.total }}</div> @selection-change="handleSelectionChange"
<div class="stat-label">总违规站点</div> style="width: 100%"
</div> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
</div> >
<div class="stat-card severe-card"> <el-table-column type="selection" width="55" />
<div class="stat-icon"><el-icon><CircleClose /></el-icon></div> <el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
<div class="stat-content"> <el-table-column prop="url" label="违规地址" min-width="200" show-overflow-tooltip>
<div class="stat-value">{{ violationStats.severe }}</div> <template #default="{ row }">
<div class="stat-label">严重违规</div> <el-link :href="row.url" target="_blank" type="danger" v-if="row.url">
</div> {{ row.url }}
</div> </el-link>
<div class="stat-card moderate-card"> <span v-else class="text-muted">无访问地址</span>
<div class="stat-icon"><el-icon><WarningFilled /></el-icon></div> </template>
<div class="stat-content"> </el-table-column>
<div class="stat-value">{{ violationStats.moderate }}</div> <el-table-column label="违规类型" width="120" align="center">
<div class="stat-label">中度违规</div> <template #default="{ row }">
</div> <el-tag type="danger" size="small" v-if="row.violation_keys && row.violation_keys.length > 0">
</div> {{ row.violation_keys.join(', ') }}
<div class="stat-card pending-card"> </el-tag>
<div class="stat-icon"><el-icon><Clock /></el-icon></div> <el-tag type="warning" size="small" v-else>
<div class="stat-content"> 检测到违规
<div class="stat-value">{{ violationStats.pending }}</div> </el-tag>
<div class="stat-label">待处理</div> </template>
</div> </el-table-column>
</div> <el-table-column label="连接类型" width="100" align="center">
</div> --> <template #default="{ row }">
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
<!-- 搜索和筛选 --> {{ getConnectTypeText(row.connect_type) }}
<el-card class="filter-container" shadow="never"> </el-tag>
<el-form :inline="true" :model="queryParams" class="search-form"> </template>
<el-form-item label="搜索容器"> </el-table-column>
<el-input v-model="queryParams.domain" placeholder="请输入容器id或服务器id" clearable /> <el-table-column prop="createTime" label="创建时间" width="180" />
</el-form-item> <el-table-column label="操作" fixed="right" align="center" width="180">
<!-- <el-form-item label="违规类型"> <template #default="{ row }">
<el-select v-model="queryParams.violationType" placeholder="请选择违规类型" clearable>
<el-option label="全部" value="" />
<el-option label="内容违规" value="content" />
<el-option label="版权侵犯" value="copyright" />
<el-option label="恶意软件" value="malware" />
<el-option label="钓鱼网站" value="phishing" />
<el-option label="其他违规" value="other" />
</el-select>
</el-form-item>
<el-form-item label="违规等级">
<el-select v-model="queryParams.severity" placeholder="请选择违规等级" clearable>
<el-option label="全部" value="" />
<el-option label="轻微" value="light" />
<el-option label="中度" value="moderate" />
<el-option label="严重" value="severe" />
</el-select>
</el-form-item>
<el-form-item label="处理状态">
<el-select v-model="queryParams.processStatus" placeholder="请选择处理状态" clearable>
<el-option label="全部" value="" />
<el-option label="待处理" value="pending" />
<el-option label="处理中" value="processing" />
<el-option label="已处理" value="processed" />
</el-select>
</el-form-item> -->
<el-form-item>
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button>
<el-button @click="resetQuery" :icon="Delete">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 违规站点列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="violationList"
@selection-change="handleSelectionChange"
style="width: 100%"
border
stripe
>
<el-table-column type="selection" width="55" />
<el-table-column prop="container_id" label="容器ID" width="280" show-overflow-tooltip />
<el-table-column prop="url" label="违规地址" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-link :href="row.url" target="_blank" type="danger" v-if="row.url">
{{ row.url }}
</el-link>
<span v-else class="text-muted">无访问地址</span>
</template>
</el-table-column>
<el-table-column label="违规类型" width="120" align="center">
<template #default="{ row }">
<el-tag type="danger" size="small" v-if="row.violation_keys && row.violation_keys.length > 0">
{{ row.violation_keys.join(', ') }}
</el-tag>
<el-tag type="warning" size="small" v-else>
检测到违规
</el-tag>
</template>
</el-table-column>
<el-table-column label="连接类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getConnectTypeColor(row.connect_type)" size="small">
{{ getConnectTypeText(row.connect_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" fixed="right" align="center">
<template #default="{ row }">
<div class="action-buttons">
<el-tooltip content="重新检查" placement="top"> <el-tooltip content="重新检查" placement="top">
<el-button type="warning" :icon="Refresh" circle size="small" @click="handleRecheck(row)" /> <el-button type="warning" link @click="handleRecheck(row)">
<el-icon><Refresh /></el-icon>检查
</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'"> <el-tooltip content="标记违规" placement="top" v-if="row.status !== 'violation'">
<el-button type="danger" :icon="Warning" circle size="small" @click="handleMarkViolation(row)" /> <el-button type="danger" link @click="handleMarkViolation(row)">
<el-icon><Warning /></el-icon>违规
</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'"> <el-tooltip content="标记正常" placement="top" v-if="row.status === 'violation'">
<el-button type="success" :icon="CircleCheck" circle size="small" @click="handleMarkNormal(row)" /> <el-button type="success" link @click="handleMarkNormal(row)">
<el-icon><CircleCheck /></el-icon>正常
</el-button>
</el-tooltip> </el-tooltip>
</div> </template>
</template> </el-table-column>
</el-table-column> </el-table>
</el-table>
<!-- 分页 -->
<!-- 分页 --> <el-pagination
<el-pagination v-model:current-page="queryParams.pageNum"
v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
layout="total, sizes, prev, pager, next, jumper" :total="pagination.total"
:total="pagination.total" @size-change="handleSizeChange"
@size-change="handleSizeChange" @current-change="handleCurrentChange"
@current-change="handleCurrentChange" background
background class="pagination"
class="pagination" />
/> </div>
</el-card> </el-card>
<!-- 违规详情对话框 --> <!-- 违规详情对话框 -->
@@ -799,7 +742,13 @@ const handleBatchProcess = () => {
// 导出数据 // 导出数据
const handleExport = () => { const handleExport = () => {
ElMessage.success('违规报告导出功能开发中...') ElMessage.success('导出功能开发中...')
}
// 查看详情
const handleView = (row) => {
currentSite.value = row
detailDialogVisible.value = true
} }
// 重新检查 // 重新检查
@@ -893,12 +842,6 @@ const handleMarkNormal = (row) => {
}).catch(() => {}) }).catch(() => {})
} }
// 查看详情
const handleView = (row) => {
currentSite.value = row
detailDialogVisible.value = true
}
// 处理违规 // 处理违规
const handleProcess = (row) => { const handleProcess = (row) => {
currentSite.value = row currentSite.value = row
@@ -907,49 +850,14 @@ const handleProcess = (row) => {
processDialogVisible.value = true processDialogVisible.value = true
} }
// 封禁站点
const handleBlock = (row) => {
ElMessageBox.confirm(
`确定要封禁站点 "${row.domain}" 吗?`,
'封禁确认',
{
confirmButtonText: '确定封禁',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 模拟API调用
row.isBlocked = true
ElMessage.success('站点已封禁')
getList()
}).catch(() => {})
}
// 解封站点
const handleUnblock = (row) => {
ElMessageBox.confirm(
`确定要解封站点 "${row.domain}" 吗?`,
'解封确认',
{
confirmButtonText: '确定解封',
cancelButtonText: '取消',
type: 'success'
}
).then(() => {
// 模拟API调用
row.isBlocked = false
ElMessage.success('站点已解封')
getList()
}).catch(() => {})
}
// 提交处理 // 提交处理
const submitProcess = () => { const submitProcess = () => {
if (!processFormRef.value) return if (!processFormRef.value) return
processFormRef.value.validate((valid) => { processFormRef.value.validate((valid) => {
if (valid) { if (valid) {
ElMessage.success('违规处理提交成功') // 模拟提交处理
ElMessage.success('处理成功')
processDialogVisible.value = false processDialogVisible.value = false
getList() getList()
} }
@@ -964,42 +872,7 @@ onMounted(() => {
<style scoped> <style scoped>
.violation-sites-container { .violation-sites-container {
padding: 20px; padding: 0;
min-height: calc(100vh - 120px);
background-color: #f5f7fa;
}
/* 页面标题样式 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.count-tag {
font-size: 13px;
}
.page-header .actions {
display: flex;
gap: 12px;
align-items: center;
} }
/* 统计卡片 */ /* 统计卡片 */
@@ -1012,18 +885,18 @@ onMounted(() => {
.stat-card { .stat-card {
background: white; background: white;
border-radius: 8px; border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 20px; padding: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: all 0.3s; transition: all 0.3s;
border: 1px solid #ebeef5; border: 1px solid #e1e8ed;
} }
.stat-card:hover { .stat-card:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.stat-icon { .stat-icon {
@@ -1039,8 +912,8 @@ onMounted(() => {
} }
.total-card .stat-icon { .total-card .stat-icon {
background-color: rgba(230, 162, 60, 0.1); background-color: rgba(64, 158, 255, 0.1);
color: #E6A23C; color: #409EFF;
} }
.severe-card .stat-icon { .severe-card .stat-icon {
@@ -1049,8 +922,8 @@ onMounted(() => {
} }
.moderate-card .stat-icon { .moderate-card .stat-icon {
background-color: rgba(255, 193, 7, 0.1); background-color: rgba(230, 162, 60, 0.1);
color: #FFC107; color: #E6A23C;
} }
.pending-card .stat-icon { .pending-card .stat-icon {
@@ -1067,56 +940,67 @@ onMounted(() => {
font-weight: 600; font-weight: 600;
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1.1; line-height: 1.1;
color: #303133;
} }
.stat-label { .stat-label {
font-size: 14px; font-size: 14px;
color: #606266; color: #909399;
} }
/* 筛选容器 */ .main-container {
.filter-container { border: 1px solid #e1e8ed;
margin-bottom: 20px; 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 { .search-form {
margin-bottom: 0; margin: 0;
} flex: 1;
/* 表格容器 */
.table-container {
margin-bottom: 20px;
}
.domain-cell {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
flex-wrap: wrap;
} }
.blocked-tag { .search-form :deep(.el-form-item) {
margin-left: 8px; margin-bottom: 0;
margin-right: 12px;
} }
.action-buttons { .action-bar {
display: flex; display: flex;
justify-content: center; gap: 12px;
gap: 8px; flex-shrink: 0;
} }
.report-count { .table-section {
color: #606266; padding: 0;
font-size: 14px;
} }
/* 分页 */
.pagination { .pagination {
margin-top: 15px; margin-top: 20px;
display: flex; padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
/* 违规详情 */ /* 详情样式 */
.violation-detail { .violation-detail {
padding: 10px 0; padding: 10px 0;
} }
@@ -1127,43 +1011,76 @@ onMounted(() => {
.detail-section h4 { .detail-section h4 {
margin-bottom: 12px; margin-bottom: 12px;
color: #303133; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #303133;
border-left: 4px solid #409EFF;
padding-left: 10px;
} }
/* 对话框底部 */ /* 表格样式优化 */
.dialog-footer { :deep(.el-table) {
display: flex; border: none;
justify-content: flex-end; 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;
} }
/* 响应式设计 */ /* 响应式设计 */
@media screen and (max-width: 1200px) { @media screen and (max-width: 992px) {
.stats-panel { .stats-panel {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.stat-card:last-child {
grid-column: span 2;
}
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.page-header .actions {
width: 100%;
flex-wrap: wrap;
}
.stats-panel { .stats-panel {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
}
.stat-card:last-child {
/* 自定义样式 */ grid-column: auto;
.text-muted { }
color: #909399;
font-style: italic; .filter-content {
flex-direction: column;
align-items: stretch;
}
.search-form {
width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-start;
}
} }
</style> </style>
+224 -92
View File
@@ -1,91 +1,116 @@
<template> <template>
<div class="discount-code-container"> <div class="discount-code-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<div class="action-bar"> <!-- 搜索和操作栏 -->
<el-button type="primary" @click="handleAdd"> <div class="filter-section">
<el-icon><Plus /></el-icon>新增优惠码 <div class="filter-content">
</el-button> <div class="action-bar">
<el-button type="success" @click="fetchDiscountList"> <el-button type="primary" @click="handleAdd">
<el-icon><Refresh /></el-icon> <el-icon><Plus /></el-icon>增优惠码
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="success" @click="fetchDiscountList">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Refresh /></el-icon>刷新
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 优惠码列表 --> <!-- 优惠码列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="discountList" <div v-for="i in 5" :key="i" class="skeleton-row">
@selection-change="handleSelectionChange" <div class="skeleton-cell skeleton-checkbox"></div>
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-code"></div>
<el-table-column type="selection" width="55" /> <div class="skeleton-cell skeleton-name"></div>
<el-table-column prop="id" label="ID" width="80" /> <div class="skeleton-cell skeleton-type"></div>
<el-table-column prop="code" label="优惠码" min-width="150" /> <div class="skeleton-cell skeleton-value"></div>
<el-table-column prop="name" label="名称" min-width="180" /> <div class="skeleton-cell skeleton-min"></div>
<el-table-column label="优惠类型" width="120"> <div class="skeleton-cell skeleton-max"></div>
<template #default="{ row }"> <div class="skeleton-cell skeleton-times"></div>
<el-tag :type="row.percentage ? 'success' : 'primary'"> <div class="skeleton-cell skeleton-action"></div>
{{ row.percentage ? '百分比折扣' : '固定金额' }} </div>
</el-tag> </div>
</template>
</el-table-column> <el-table
<el-table-column label="优惠值" width="120"> v-else
<template #default="{ row }"> v-loading="loading"
<span v-if="row.percentage" class="discount-value">{{ (row.percentage / 100).toFixed(0) }}%</span> :data="discountList"
<span v-else class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span> @selection-change="handleSelectionChange"
</template> style="width: 100%"
</el-table-column> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column label="最低消费" width="120"> >
<template #default="{ row }"> <el-table-column type="selection" width="55" />
¥{{ (row.minAmount / 100).toFixed(2) }} <el-table-column prop="id" label="ID" width="80" />
</template> <el-table-column prop="code" label="优惠码" min-width="150" />
</el-table-column> <el-table-column prop="name" label="名称" min-width="180" />
<el-table-column label="最大抵扣" width="120"> <el-table-column label="优惠类型" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span> <el-tag :type="row.percentage ? 'success' : 'primary'">
<span v-else>-</span> {{ row.percentage ? '百分比折扣' : '固定金额' }}
</template> </el-tag>
</el-table-column> </template>
<el-table-column prop="maxTimes" label="最大使用次数" width="120" /> </el-table-column>
<el-table-column prop="userTimes" label="单用户次数" width="120" /> <el-table-column label="优惠值" width="120">
<el-table-column label="可叠加" width="100" align="center"> <template #default="{ row }">
<template #default="{ row }"> <span v-if="row.percentage" class="discount-value">{{ (row.percentage / 100).toFixed(0) }}%</span>
<el-icon v-if="row.canStacking" color="#67c23a" :size="20"><SuccessFilled /></el-icon> <span v-else class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="最低消费" width="120">
<el-table-column label="续费可用" width="100" align="center"> <template #default="{ row }">
<template #default="{ row }"> ¥{{ (row.minAmount / 100).toFixed(2) }}
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon> </template>
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon> </el-table-column>
</template> <el-table-column label="最大抵扣" width="120">
</el-table-column> <template #default="{ row }">
<el-table-column label="操作" width="200" fixed="right"> <span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
<template #default="{ row }"> <span v-else>-</span>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> </template>
<el-button type="success" link @click="handleView(row)">查看</el-button> </el-table-column>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-table-column prop="maxTimes" label="最大使用次数" width="120" />
</template> <el-table-column prop="userTimes" label="单用户次数" width="120" />
</el-table-column> <el-table-column label="可叠加" width="100" align="center">
</el-table> <template #default="{ row }">
<el-icon v-if="row.canStacking" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
<!-- 分页 --> <el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
<el-pagination </template>
v-model:current-page="queryParams.page" </el-table-column>
v-model:page-size="queryParams.count" <el-table-column label="续费可用" width="100" align="center">
:page-sizes="[10, 20, 50, 100]" <template #default="{ row }">
layout="total, sizes, prev, pager, next, jumper" <el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
:total="total" <el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
@size-change="handleSizeChange" </template>
@current-change="handleCurrentChange" </el-table-column>
background <el-table-column label="操作" width="200" fixed="right">
class="pagination" <template #default="{ row }">
/> <div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 优惠码表单对话框 --> <!-- 优惠码表单对话框 -->
@@ -93,6 +118,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'" :title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'"
width="700px" width="700px"
append-to-body
> >
<el-form <el-form
ref="discountFormRef" ref="discountFormRef"
@@ -161,8 +187,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
@@ -489,18 +517,40 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
} }
.amount { .amount {
@@ -516,9 +566,91 @@ onMounted(() => {
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-checkbox { width: 55px; }
.skeleton-id { width: 80px; }
.skeleton-code { width: 150px; }
.skeleton-name { width: 180px; }
.skeleton-type { width: 120px; }
.skeleton-value { width: 120px; }
.skeleton-min { width: 120px; }
.skeleton-max { width: 120px; }
.skeleton-times { width: 120px; }
.skeleton-action { width: 200px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style> </style>
<style> <style>
+269 -116
View File
@@ -1,108 +1,133 @@
<template> <template>
<div class="discount-goods-container"> <div class="discount-goods-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="代金卷"> <div class="filter-section">
<el-select <div class="filter-content">
v-model="queryParams.code_id" <el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
placeholder="请选择代金券" <el-form-item label="代金卷" v-if="!codeId">
filterable <el-select
clearable v-model="queryParams.code_id"
style="width: 280px" placeholder="请选择代金券"
> filterable
<el-option clearable
v-for="item in voucherListOptions" style="width: 280px"
:key="item.id" >
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`" <el-option
:value="item.id" v-for="item in voucherListOptions"
/> :key="item.id"
</el-select> :label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
</el-form-item> :value="item.id"
<el-form-item> />
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id"> </el-select>
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleAdd"> </el-form-item>
<el-icon><Plus /></el-icon>新增商品关联 </el-form>
</el-button> <div class="action-bar">
<el-button type="success" @click="fetchGoodsList"> <el-button type="primary" @click="handleAdd">
<el-icon><Refresh /></el-icon> <el-icon><Plus /></el-icon>增商品关联
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="success" @click="fetchGoodsList">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Refresh /></el-icon>刷新
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 商品关联列表 --> <!-- 商品关联列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="goodsList" <div v-for="i in 5" :key="i" class="skeleton-row">
@selection-change="handleSelectionChange" <div class="skeleton-cell skeleton-checkbox"></div>
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-discount-id"></div>
<el-table-column type="selection" width="55" /> <div class="skeleton-cell skeleton-related-id"></div>
<el-table-column prop="id" label="ID" width="80" /> <div class="skeleton-cell skeleton-name"></div>
<el-table-column prop="discountId" label="代金券ID" width="120" /> <div class="skeleton-cell skeleton-type"></div>
<el-table-column label="关联对象ID" width="120"> <div class="skeleton-cell skeleton-note"></div>
<template #default="{ row }"> <div class="skeleton-cell skeleton-price"></div>
{{ row.goodId || row.goodGroupId || '-' }} <div class="skeleton-cell skeleton-time"></div>
</template> <div class="skeleton-cell skeleton-action"></div>
</el-table-column> </div>
<el-table-column label="名称" min-width="200"> </div>
<template #default="{ row }">
{{ row.good?.name || row.goodGroup?.name || '-' }} <el-table
</template> v-else
</el-table-column> v-loading="loading"
<el-table-column label="类型" width="120"> :data="goodsList"
<template #default="{ row }"> @selection-change="handleSelectionChange"
<el-tag :type="getGoodsTypeTagByRow(row)"> style="width: 100%"
{{ getGoodsTypeNameByRow(row) }} :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
</el-tag> >
</template> <el-table-column type="selection" width="55" />
</el-table-column> <el-table-column prop="id" label="ID" width="80" />
<el-table-column label="备注" min-width="150"> <el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
<template #default="{ row }"> <el-table-column label="关联对象ID" width="120">
{{ row.good?.table || row.goodGroup?.note || '-' }} <template #default="{ row }">
</template> {{ row.goodId || row.goodGroupId || '-' }}
</el-table-column> </template>
<el-table-column label="商品价格" width="120"> </el-table-column>
<template #default="{ row }"> <el-table-column label="名称" min-width="200">
<span v-if="row.good?.price" class="price">¥{{ (row.good.price / 100).toFixed(2) }}</span> <template #default="{ row }">
<span v-else>-</span> {{ row.good?.name || row.goodGroup?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column label="类型" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} <el-tag :type="getGoodsTypeTagByRow(row)">
</template> {{ getGoodsTypeNameByRow(row) }}
</el-table-column> </el-tag>
<el-table-column label="操作" width="200" fixed="right"> </template>
<template #default="{ row }"> </el-table-column>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-table-column label="备注" min-width="150">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <template #default="{ row }">
</template> {{ row.good?.table || row.goodGroup?.note || '-' }}
</el-table-column> </template>
</el-table> </el-table-column>
<el-table-column label="商品价格" width="120">
<!-- 分页 --> <template #default="{ row }">
<el-pagination <span v-if="row.good?.price" class="price">¥{{ (row.good.price / 100).toFixed(2) }}</span>
v-model:current-page="queryParams.page" <span v-else>-</span>
v-model:page-size="queryParams.count" </template>
:page-sizes="[10, 20, 50, 100]" </el-table-column>
layout="total, sizes, prev, pager, next, jumper" <el-table-column label="创建时间" width="180">
:total="total" <template #default="{ row }">
@size-change="handleSizeChange" {{ formatDate(row.CreatedAt) }}
@current-change="handleCurrentChange" </template>
background </el-table-column>
class="pagination" <el-table-column label="操作" width="200" fixed="right">
/> <template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 添加/编辑商品关联对话框 --> <!-- 添加/编辑商品关联对话框 -->
@@ -110,6 +135,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品关联' : '编辑商品关联'" :title="dialogType === 'add' ? '新增商品关联' : '编辑商品关联'"
width="600px" width="600px"
append-to-body
> >
<el-form <el-form
ref="formRef" ref="formRef"
@@ -123,7 +149,7 @@
placeholder="请选择代金券" placeholder="请选择代金券"
filterable filterable
clearable clearable
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit' || !!codeId"
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
@@ -198,15 +224,17 @@
</template> </template>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import { import {
@@ -221,13 +249,27 @@ import {
getProductGroupList getProductGroupList
} from '@/api/admin/product' } from '@/api/admin/product'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// 查询参数 // 查询参数
const queryParams = reactive({ const queryParams = reactive({
code_id: '', code_id: props.codeId || '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchGoodsList()
}
})
// 表单数据 // 表单数据
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
@@ -687,27 +729,52 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
} }
.pagination { .action-buttons {
margin-top: 24px; display: flex;
justify-content: flex-end; gap: 8px;
align-items: center;
} }
.price { .price {
@@ -715,5 +782,91 @@ onMounted(() => {
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 14px;
} }
</style>
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-checkbox { width: 55px; }
.skeleton-id { width: 80px; }
.skeleton-discount-id { width: 120px; }
.skeleton-related-id { width: 120px; }
.skeleton-name { width: 200px; }
.skeleton-type { width: 120px; }
.skeleton-note { width: 150px; }
.skeleton-price { width: 120px; }
.skeleton-time { width: 180px; }
.skeleton-action { width: 200px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+274 -244
View File
@@ -1,92 +1,119 @@
<template> <template>
<div class="discount-users-container"> <div class="discount-users-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="代金卷"> <div class="filter-section">
<el-select <div class="filter-content">
v-model="queryParams.code_id" <el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
placeholder="请选择代金券" <el-form-item label="代金卷" v-if="!codeId">
filterable <el-select
clearable v-model="queryParams.code_id"
style="width: 280px" placeholder="请选择代金券"
> filterable
<el-option clearable
v-for="item in voucherListOptions" style="width: 280px"
:key="item.id" >
:label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`" <el-option
:value="item.id" v-for="item in voucherListOptions"
/> :key="item.id"
</el-select> :label="`${item.name} (¥${(item.amount / 100).toFixed(2)})`"
</el-form-item> :value="item.id"
<el-form-item> />
<el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id"> </el-select>
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery" :disabled="!queryParams.code_id">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleAdd"> </el-form-item>
<el-icon><Plus /></el-icon>新增用户关联 </el-form>
</el-button> <div class="action-bar">
<el-button type="success" @click="fetchUsersList"> <el-button type="primary" @click="handleAdd">
<el-icon><Refresh /></el-icon> <el-icon><Plus /></el-icon>增用户关联
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="success" @click="fetchUsersList">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Refresh /></el-icon>刷新
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 用户关联列表 --> <!-- 用户关联列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="usersList" <div v-for="i in 5" :key="i" class="skeleton-row">
@selection-change="handleSelectionChange" <div class="skeleton-cell skeleton-checkbox"></div>
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-discount-id"></div>
<el-table-column type="selection" width="55" /> <div class="skeleton-cell skeleton-related-id"></div>
<el-table-column prop="id" label="ID" width="80" /> <div class="skeleton-cell skeleton-type"></div>
<el-table-column prop="discountId" label="代金券ID" width="120" /> <div class="skeleton-cell skeleton-time"></div>
<el-table-column label="关联对象ID" width="130"> <div class="skeleton-cell skeleton-action"></div>
<template #default="{ row }"> </div>
{{ row.userId || row.userGroupId || '-' }} </div>
</template>
</el-table-column> <el-table
<el-table-column label="类型" width="120"> v-else
<template #default="{ row }"> v-loading="loading"
<el-tag :type="getUserTypeTagByRow(row)"> :data="usersList"
{{ getUserTypeNameByRow(row) }} @selection-change="handleSelectionChange"
</el-tag> style="width: 100%"
</template> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
</el-table-column> >
<el-table-column label="创建时间" width="180"> <el-table-column type="selection" width="55" />
<template #default="{ row }"> <el-table-column prop="id" label="ID" width="80" />
{{ formatDate(row.CreatedAt) }} <el-table-column prop="discountId" label="代金券ID" width="120" v-if="!codeId" />
</template> <el-table-column label="用户名" min-width="150">
</el-table-column> <template #default="{ row }">
<el-table-column label="操作" width="200" fixed="right"> {{ row?.user?.user_name || '-' }}
<template #default="{ row }"> </template>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> </el-table-column>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-table-column label="手机号" min-width="150">
</template> <template #default="{ row }">
</el-table-column> {{ row?.user?.phone || '-' }}
</el-table> </template>
</el-table-column>
<!-- 分页 --> <el-table-column label="邮箱" min-width="150">
<el-pagination <template #default="{ row }">
v-model:current-page="queryParams.page" {{ row?.user?.email || '-' }}
v-model:page-size="queryParams.count" </template>
:page-sizes="[10, 20, 50, 100]" </el-table-column>
layout="total, sizes, prev, pager, next, jumper" <el-table-column label="类型" width="120">
:total="total" <template #default="{ row }">
@size-change="handleSizeChange" <el-tag :type="getUserTypeTagByRow(row)">
@current-change="handleCurrentChange" {{ getUserTypeNameByRow(row) }}
background </el-tag>
class="pagination" </template>
/> </el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 添加/编辑用户关联对话框 --> <!-- 添加/编辑用户关联对话框 -->
@@ -94,6 +121,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增用户关联' : '编辑用户关联'" :title="dialogType === 'add' ? '新增用户关联' : '编辑用户关联'"
width="600px" width="600px"
append-to-body
> >
<el-form <el-form
ref="formRef" ref="formRef"
@@ -173,11 +201,11 @@
<el-form-item v-if="form.select_type === 'user'" label="用户ID" prop="user_id"> <el-form-item v-if="form.select_type === 'user'" label="用户ID" prop="user_id">
<div class="user-selector-wrapper"> <div class="user-selector-wrapper">
<div class="selected-user-display" v-if="form.user_id"> <div class="selected-user-display" v-if="form.user_id">
<el-tage type="primary" closable @close="clearSelectedUser"> <el-tag type="primary" closable @close="clearSelectedUser">
{{ getSelectedUserName() }} {{ getSelectedUserName() }}
</el-tage> </el-tag>
</div> </div>
<el-button type="primary" plan @click="openUserSelector" style="width: 100%;"> <el-button type="primary" plain @click="openUserSelector" style="width: 100%;">
<el-icon><User /></el-icon> <el-icon><User /></el-icon>
{{ form.user_id ? '重新选择用户' : '选择用户' }} {{ form.user_id ? '重新选择用户' : '选择用户' }}
</el-button> </el-button>
@@ -193,85 +221,24 @@
</template> </template>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <UserSelector
v-model="userSelectorVisible" v-model:visible="userSelectorVisible"
title="选择用户" @select="confirmUserSelection"
width="800px" />
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import { import {
@@ -285,14 +252,29 @@ import {
getUserList, getUserList,
getUserGroupList getUserGroupList
} from '@/api/admin/user' } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// //
const queryParams = reactive({ const queryParams = reactive({
code_id: '', code_id: props.codeId || '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUsersList()
}
})
// //
const form = reactive({ const form = reactive({
id: undefined, id: undefined,
@@ -336,15 +318,6 @@ const userGroupOptions = ref([]) // 用户组列表选项
// //
const userSelectorVisible = ref(false) const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) //
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// //
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
@@ -360,25 +333,24 @@ const formatDate = (dateStr) => {
// //
const getUserTypeNameByRow = (row) => { const getUserTypeNameByRow = (row) => {
// userId 0
if (row.userId && row.userId !== 0) { //user
if(row.user){
return '用户' return '用户'
} }else{
// userGroupId 0
if (row.userGroupId && row.userGroupId !== 0) {
return '用户组' return '用户组'
} }
return '-' return '-'
} }
// //
const getUserTypeTagByRow = (row) => { const getUserTypeTagByRow = (row) => {
//
if (row.userId && row.userId !== 0) {
if(row.user){
return 'primary' return 'primary'
} }else{
//
if (row.userGroupId && row.userGroupId !== 0) {
return 'warning' return 'warning'
} }
return 'info' return 'info'
@@ -441,70 +413,19 @@ const fetchUserGroupList = async () => {
// //
const openUserSelector = () => { const openUserSelector = () => {
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
//
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
//
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
} }
// //
const confirmUserSelection = () => { const confirmUserSelection = (user) => {
if (!selectedUserTemp.value) { if (!user) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
form.selected_user = selectedUserTemp.value.UserId form.selected_user = user.UserId
form.user_id = selectedUserTemp.value.UserId form.user_id = user.UserId
// userOptions // userOptions
if (!userOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) { if (!userOptions.value.find(u => u.UserId === user.UserId)) {
userOptions.value.push(selectedUserTemp.value) userOptions.value.push(user)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
ElMessage.success('用户选择成功') ElMessage.success('用户选择成功')
@@ -663,6 +584,7 @@ const handleEdit = (row) => {
}) })
// //
fetchUserList() fetchUserList()
fetchUserGroupList()
} }
// //
@@ -818,29 +740,100 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 用户选择器样式 */ /* 用户选择器样式 */
.user-selector-wrapper { .user-selector-wrapper {
width: 100%; width: 100%;
@@ -879,5 +872,42 @@ onMounted(() => {
:deep(.current-row) { :deep(.current-row) {
background-color: #ecf5ff !important; background-color: #ecf5ff !important;
} }
</style>
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-checkbox { width: 55px; }
.skeleton-id { width: 80px; }
.skeleton-discount-id { width: 120px; }
.skeleton-related-id { width: 130px; }
.skeleton-type { width: 120px; }
.skeleton-time { width: 180px; }
.skeleton-action { width: 200px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+85 -33
View File
@@ -2,8 +2,8 @@
<div class="user-voucher-container"> <div class="user-voucher-container">
<!-- 搜索和操作栏 --> <!-- 搜索和操作栏 -->
<el-card class="filter-container" shadow="never"> <el-card class="filter-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <el-form :inline="true" :model="queryParams" class="search-form" v-if="!codeId">
<el-form-item label="代金券"> <el-form-item label="代金券" v-if="!codeId">
<el-select <el-select
v-model="queryParams.code_id" v-model="queryParams.code_id"
placeholder="请选择代金券" placeholder="请选择代金券"
@@ -54,37 +54,37 @@
{{ row.Id || row.id }} {{ row.Id || row.id }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="用户ID" width="100"> <el-table-column label="用户ID" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.UserId || row.userId }} {{ row.UserId || row.userId }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券ID" width="100"> <el-table-column label="代金券ID" width="100" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discountId }} {{ row.discountId }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券名称" min-width="150"> <el-table-column label="代金券名称" min-width="150" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.name || '-' }} {{ row.discount?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="面额" width="120"> <el-table-column label="面额" min-width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="已使用/最大次数" width="150"> <el-table-column label="已使用/最大次数" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
<el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag> <el-tag type="info">{{ row.useTimes || 0 }} / {{ row.maxUseTimes || row.discount?.maxTimes || 0 }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="过期时间" width="180"> <el-table-column label="过期时间" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.expireAt) }} {{ formatDate(row.expireAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column label="创建时间" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} {{ formatDate(row.CreatedAt) }}
</template> </template>
@@ -134,7 +134,7 @@
<el-select <el-select
v-model="addForm.voucher_id" v-model="addForm.voucher_id"
placeholder="请选择代金券" placeholder="请选择代金券"
:disabled="addForm.discount_type === 'code'" :disabled="addForm.discount_type === 'code' || !!codeId"
filterable filterable
clearable clearable
style="width: 100%" style="width: 100%"
@@ -178,25 +178,22 @@
</el-form-item> </el-form-item>
<el-form-item label="用户" prop="user_id"> <el-form-item label="用户" prop="user_id">
<el-select <div class="user-selector-wrapper">
v-model="addForm.user_id" <div class="selected-user-display" v-if="addForm.user_id">
placeholder="请选择用户" <el-tag type="primary" closable @close="clearSelectedUser">
:disabled="addForm.target_type === 'group'" {{ getSelectedUserName() }}
filterable </el-tag>
clearable </div>
remote <el-button
:remote-method="searchUsers" type="primary"
:loading="userSearchLoading" plain
style="width: 100%" @click="openUserSelector"
@change="handleUserChange" style="width: 100%"
> >
<el-option <el-icon><User /></el-icon>
v-for="item in userOptions" {{ addForm.user_id ? '重新选择用户' : '选择用户' }}
:key="item.UserId" </el-button>
:label="`${item.UserName} (ID: ${item.UserId})`" </div>
:value="item.UserId"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="用户组" prop="group_id"> <el-form-item label="用户组" prop="group_id">
@@ -268,13 +265,19 @@
<el-button type="primary" @click="submitEdit">确定</el-button> <el-button type="primary" @click="submitEdit">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 -->
<UserSelector
v-model:visible="userSelectorVisible"
@select="confirmUserSelection"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue' import { Delete, Search, Plus, Refresh, User } from '@element-plus/icons-vue'
import { import {
getUserVoucherList, getUserVoucherList,
addUserVoucher, addUserVoucher,
@@ -285,14 +288,29 @@ import {
allocateVoucher allocateVoucher
} from '@/api/admin/discount' } from '@/api/admin/discount'
import { getUserList, getUserGroupList } from '@/api/admin/user' import { getUserList, getUserGroupList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// //
const queryParams = reactive({ const queryParams = reactive({
code_id: undefined, code_id: props.codeId || undefined,
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchUserVoucherList()
}
})
// //
const addForm = reactive({ const addForm = reactive({
discount_type: 'coupon', // coupon-, code- discount_type: 'coupon', // coupon-, code-
@@ -321,6 +339,7 @@ const groupOptions = ref([]) // 用户组选项
const userSearchLoading = ref(false) // const userSearchLoading = ref(false) //
const submitLoading = ref(false) // const submitLoading = ref(false) //
const dataList = ref([]) // const dataList = ref([]) //
const userSelectorVisible = ref(false)
// //
const editForm = reactive({ const editForm = reactive({
@@ -623,6 +642,36 @@ const handleGroupChange = (val) => {
} }
} }
//
const openUserSelector = () => {
userSelectorVisible.value = true
}
//
const confirmUserSelection = (user) => {
if (!user) {
ElMessage.warning('请选择一个用户')
return
}
addForm.user_id = user.UserId
// userOptions
if (!userOptions.value.find(u => u.UserId === user.UserId)) {
userOptions.value.push(user)
}
userSelectorVisible.value = false
}
//
const clearSelectedUser = () => {
addForm.user_id = undefined
}
//
const getSelectedUserName = () => {
const user = userOptions.value.find(u => u.UserId === addForm.user_id)
return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${addForm.user_id}`
}
// //
const handleAdd = async () => { const handleAdd = async () => {
addDialogVisible.value = true addDialogVisible.value = true
@@ -630,7 +679,7 @@ const handleAdd = async () => {
// //
Object.assign(addForm, { Object.assign(addForm, {
discount_type: 'coupon', discount_type: 'coupon',
voucher_id: undefined, voucher_id: props.codeId || undefined,
code_id: undefined, code_id: undefined,
target_type: 'user', target_type: 'user',
user_id: undefined, user_id: undefined,
@@ -837,6 +886,9 @@ onMounted(() => {
// //
fetchVoucherListOptions() fetchVoucherListOptions()
fetchDiscountList() fetchDiscountList()
if (queryParams.code_id) {
fetchUserVoucherList()
}
}) })
</script> </script>
+11 -1
View File
@@ -63,9 +63,10 @@
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon> <el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
<el-button type="success" link @click="handleView(row)">查看</el-button> <el-button type="success" link @click="handleView(row)">查看</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template> </template>
@@ -166,8 +167,10 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue' import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import { import {
@@ -180,6 +183,8 @@ import {
import { timeToTimestamp } from '@/utils/tool' import { timeToTimestamp } from '@/utils/tool'
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue' import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
const router = useRouter()
// //
const queryParams = reactive({ const queryParams = reactive({
discount_type: 'coupon', // coupon discount_type: 'coupon', // coupon
@@ -312,6 +317,11 @@ const handleEdit = (row) => {
}) })
} }
//
const handleManage = (row) => {
router.push(`/marketing/voucher/${row.id}/manage`)
}
// //
const handleView = async (row) => { const handleView = async (row) => {
try { try {
+42 -144
View File
@@ -46,8 +46,8 @@
<el-table-column prop="user_id" label="用户ID" width="100" /> <el-table-column prop="user_id" label="用户ID" width="100" />
<el-table-column prop="username" label="用户名" width="150" /> <el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="email" label="邮箱" min-width="200" /> <el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column prop="discount_id" label="代金券ID" width="120" /> <el-table-column prop="discount_id" label="代金券ID" width="120" v-if="!codeId" />
<el-table-column prop="discount_name" label="代金券名称" min-width="180" /> <el-table-column prop="discount_name" label="代金券名称" min-width="180" v-if="!codeId" />
<el-table-column label="优惠金额" width="120"> <el-table-column label="优惠金额" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount_amount ? (row.discount_amount / 100).toFixed(2) : '0.00' }}</span>
@@ -133,74 +133,10 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <UserSelector
v-model="userSelectorVisible" v-model:visible="userSelectorVisible"
title="选择用户" @select="confirmUserSelection"
width="800px" />
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
<!-- 统计卡片 --> <!-- 统计卡片 -->
<el-row :gutter="20" style="margin-top: 20px"> <el-row :gutter="20" style="margin-top: 20px">
@@ -237,20 +173,36 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Refresh, Download } from '@element-plus/icons-vue' import { Search, Refresh, Download } from '@element-plus/icons-vue'
import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount' import { getUserVoucherHistory, getDiscountCodeList } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user' import { getUserList } from '@/api/admin/user'
import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// //
const queryParams = reactive({ const queryParams = reactive({
user_id: undefined, user_id: undefined,
code_id: props.codeId || undefined,
id: '', id: '',
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
fetchHistoryList()
}
})
// //
const loading = ref(false) const loading = ref(false)
const historyList = ref([]) const historyList = ref([])
@@ -261,15 +213,6 @@ const currentDetail = ref({})
const discountOptions = ref([]) const discountOptions = ref([])
const selectorType = ref('query') const selectorType = ref('query')
const userSelectorVisible = ref(false) const userSelectorVisible = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
const selectedUserTemp = ref(null)
const userSelectorLoading = ref(false)
const UserOptions = ref([]) const UserOptions = ref([])
// //
@@ -371,86 +314,41 @@ const resetUserSearch = () => {
// fetchUserSelectorList() // fetchUserSelectorList()
} }
//
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
}
//
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
}
// //
const confirmUserSelection = () => { const confirmUserSelection = (user) => {
if (!selectedUserTemp.value) { if (!user) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
if (selectorType.value === 'query') { if (selectorType.value === 'query') {
// //
queryParams.user_id = selectedUserTemp.value.UserId queryParams.user_id = user.UserId
} else { } else {
// //
editForm.user_id = selectedUserTemp.value.UserId editForm.user_id = user.UserId
} }
// UserOptions // UserOptions
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) { if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
UserOptions.value.push(selectedUserTemp.value) UserOptions.value.push(user)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
ElMessage.success('用户选择成功') ElMessage.success('用户选择成功')
} }
//
const openQueryUserSelector = () => {
selectorType.value = 'query'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const openEditUserSelector = () => {
selectorType.value = 'edit'
userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
//
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
}
//
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
// //
const handleQuery = () => { const handleQuery = () => {
@@ -461,7 +359,7 @@ const handleQuery = () => {
// //
const resetQuery = () => { const resetQuery = () => {
queryParams.user_id = undefined queryParams.user_id = undefined
queryParams.discount_id = undefined queryParams.code_id = undefined
queryParams.id = '' queryParams.id = ''
queryParams.page = 1 queryParams.page = 1
fetchHistoryList() fetchHistoryList()
+48 -167
View File
@@ -40,51 +40,51 @@
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="Id" label="ID" width="80" /> <el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="UserId" label="用户ID" width="100" /> <el-table-column prop="UserId" label="用户ID" min-width="100" />
<el-table-column label="代金券ID" width="120"> <el-table-column label="代金券ID" min-width="110" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discountId || '-' }} {{ row.discountId || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券名称" min-width="180"> <el-table-column label="代金券名称" min-width="180" v-if="!codeId" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.name || '-' }} {{ row.discount?.name || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="代金券编码" width="150"> <el-table-column label="代金券编码" min-width="150" v-if="!codeId">
<template #default="{ row }"> <template #default="{ row }">
{{ row.discount?.code || '-' }} {{ row.discount?.code || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="面额" width="120"> <el-table-column label="面额" min-width="110">
<template #default="{ row }"> <template #default="{ row }">
<span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span> <span class="amount">¥{{ row.discount?.amount ? (row.discount.amount / 100).toFixed(2) : '0.00' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="useTimes" label="已使用次数" width="120" /> <el-table-column prop="useTimes" label="已使用" min-width="100" />
<el-table-column prop="maxUseTimes" label="最大使用次数" width="120" /> <el-table-column prop="maxUseTimes" label="最大使用" min-width="100" />
<el-table-column label="状态" width="100"> <el-table-column label="状态" min-width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getStatusType(row)"> <el-tag :type="getStatusType(row)" size="small">
{{ getStatusText(row) }} {{ getStatusText(row) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="过期时间" width="180"> <el-table-column label="过期时间" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.expireAt) }} {{ formatDate(row.expireAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column label="创建时间" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
{{ formatDate(row.CreatedAt) }} {{ formatDate(row.CreatedAt) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="210" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看详情</el-button> <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button> <el-button type="warning" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -203,81 +203,17 @@
</el-dialog> </el-dialog>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <UserSelector
v-model="userSelectorVisible" v-model:visible="userSelectorVisible"
title="选择用户" @select="confirmUserSelection"
width="800px" />
class="user-selector-dialog"
>
<!-- 搜索栏 -->
<div class="selector-search">
<el-input
v-model="userSearchParams.key"
placeholder="搜索用户名或ID"
clearable
@keyup.enter="searchUsers"
style="width: 300px; margin-right: 12px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="searchUsers">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="resetUserSearch">重置</el-button>
</div>
<!-- 用户表格 -->
<el-table
v-loading="userSelectorLoading"
:data="userSelectorList"
highlight-current-row
@current-change="handleUserSelectChange"
style="width: 100%; margin-top: 16px"
:height="400"
>
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="Email" label="邮箱" min-width="180" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.Status === 1 ? 'success' : 'danger'" size="small">
{{ row.Status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="userSearchParams.page"
v-model:page-size="userSearchParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="userSelectorTotal"
@size-change="handleUserSelectorSizeChange"
@current-change="handleUserSelectorPageChange"
background
class="selector-pagination"
/>
<template #footer>
<el-button @click="userSelectorVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUserSelection" :disabled="!selectedUserTemp">
确定选择
</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Download, Plus, User } from '@element-plus/icons-vue' import { Search, Refresh, Plus, User } from '@element-plus/icons-vue'
import { import {
getUserVoucherList, getUserVoucherList,
allocateVoucher, allocateVoucher,
@@ -285,14 +221,34 @@ import {
deleteUserVoucher, deleteUserVoucher,
getDiscountCodeList getDiscountCodeList
} from '@/api/admin/discount' } from '@/api/admin/discount'
import { getUserList } from '@/api/admin/user' import UserSelector from '@/components/UserSelector/index.vue'
const props = defineProps({
codeId: {
type: [String, Number],
default: ''
}
})
// //
const queryParams = reactive({ const queryParams = reactive({
user_id: undefined, user_id: undefined,
code_id: props.codeId || undefined,
page: 1, page: 1,
count: 10 count: 10
}) })
watch(() => props.codeId, (newVal) => {
if (newVal) {
queryParams.code_id = newVal
// code_id API code_id
// API user_id
if (queryParams.user_id) {
fetchHoldersList()
}
}
})
// //
const loading = ref(false) const loading = ref(false)
const holdersList = ref([]) const holdersList = ref([])
@@ -307,16 +263,7 @@ const discountOptions = ref([])
// //
const userSelectorVisible = ref(false) const userSelectorVisible = ref(false)
const userSelectorLoading = ref(false)
const userSelectorList = ref([])
const userSelectorTotal = ref(0)
const selectedUserTemp = ref(null) //
const selectorType = ref('query') // 'query' 'edit' const selectorType = ref('query') // 'query' 'edit'
const userSearchParams = reactive({
key: '',
page: 1,
count: 10
})
// //
const editForm = reactive({ const editForm = reactive({
@@ -448,101 +395,36 @@ const handleExport = () => {
ElMessage.info('导出功能开发中...') ElMessage.info('导出功能开发中...')
} }
//
const fetchUserList = async () => {
const res = await getUserList({
page: 1,
count: 10000,
key: ''
})
UserOptions.value = res.data.data?.data || []
}
// //
const openQueryUserSelector = () => { const openQueryUserSelector = () => {
selectorType.value = 'query' selectorType.value = 'query'
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
} }
// //
const openEditUserSelector = () => { const openEditUserSelector = () => {
selectorType.value = 'edit' selectorType.value = 'edit'
userSelectorVisible.value = true userSelectorVisible.value = true
selectedUserTemp.value = null
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const fetchUserSelectorList = async () => {
userSelectorLoading.value = true
try {
const res = await getUserList(userSearchParams)
console.log('用户选择器列表:', res.data)
if (res.data.code === 200) {
userSelectorList.value = res.data.data?.data || []
userSelectorTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败')
} finally {
userSelectorLoading.value = false
}
}
//
const searchUsers = () => {
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const resetUserSearch = () => {
userSearchParams.key = ''
userSearchParams.page = 1
fetchUserSelectorList()
}
//
const handleUserSelectChange = (row) => {
selectedUserTemp.value = row
}
//
const handleUserSelectorSizeChange = (size) => {
userSearchParams.count = size
fetchUserSelectorList()
}
const handleUserSelectorPageChange = (page) => {
userSearchParams.page = page
fetchUserSelectorList()
} }
// //
const confirmUserSelection = () => { const confirmUserSelection = (user) => {
if (!selectedUserTemp.value) { if (!user) {
ElMessage.warning('请选择一个用户') ElMessage.warning('请选择一个用户')
return return
} }
if (selectorType.value === 'query') { if (selectorType.value === 'query') {
// //
queryParams.user_id = selectedUserTemp.value.UserId queryParams.user_id = user.UserId
} else { } else {
// //
editForm.user_id = selectedUserTemp.value.UserId editForm.user_id = user.UserId
} }
// UserOptions // UserOptions
if (!UserOptions.value.find(u => u.UserId === selectedUserTemp.value.UserId)) { if (!UserOptions.value.find(u => u.UserId === user.UserId)) {
UserOptions.value.push(selectedUserTemp.value) UserOptions.value.push(user)
} }
userSelectorVisible.value = false userSelectorVisible.value = false
@@ -692,7 +574,6 @@ const submitEditForm = () => {
// //
onMounted(() => { onMounted(() => {
fetchUserList()
fetchDiscountList() fetchDiscountList()
if (queryParams.user_id) { if (queryParams.user_id) {
fetchHoldersList() fetchHoldersList()
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="voucher-management-container">
<div class="header">
<el-page-header @back="goBack">
<template #content>
<span class="text-large font-600 mr-3">代金券管理 (ID: {{ voucherId }})</span>
</template>
</el-page-header>
</div>
<el-card class="mt-4" shadow="never">
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="用户分发管理" name="user-distribution">
<UserVoucher :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="商品关联管理" name="discount-goods">
<DiscountGoods :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户关联管理" name="discount-users">
<DiscountUsers :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户信息管理" name="user-info">
<VoucherHolders :code-id="voucherId" />
</el-tab-pane>
<el-tab-pane label="用户使用记录" name="user-history">
<VoucherHistory :code-id="voucherId" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import UserVoucher from './UserVoucher.vue'
import DiscountGoods from './DiscountGoods.vue'
import DiscountUsers from './DiscountUsers.vue'
import VoucherHolders from './VoucherHolders.vue'
import VoucherHistory from './VoucherHistory.vue'
const route = useRoute()
const router = useRouter()
const activeTab = ref('user-distribution')
const voucherId = computed(() => route.params.id)
const goBack = () => {
router.push('/marketing/voucher')
}
</script>
<style scoped>
.voucher-management-container {
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.mt-4 {
margin-top: 16px;
}
</style>
+218 -148
View File
@@ -1,131 +1,115 @@
<template> <template>
<div class="order-list-container"> <div class="order-list-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<!-- <el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="订单号"> <div class="filter-section">
<el-input v-model="queryParams.order_no" placeholder="请输入订单号" clearable style="width: 200px" /> <div class="filter-content">
</el-form-item> <div class="action-bar">
<el-form-item label="用户ID"> <el-button type="primary" @click="handleAdd">
<el-input v-model="queryParams.user_id" placeholder="请输入用户ID" clearable style="width: 150px" /> <el-icon><Plus /></el-icon>新增订单
</el-form-item> </el-button>
<el-form-item label="订单状态"> <el-button type="success" @click="fetchOrderList">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 150px"> <el-icon><Refresh /></el-icon>刷新
<el-option label="待支付" value="0" /> </el-button>
<el-option label="已支付" value="1" /> </div>
<el-option label="已完成" value="2" /> </div>
<el-option label="已取消" value="3" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</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-form-item>
</el-form> -->
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增订单
</el-button>
<el-button type="success" @click="fetchOrderList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<!-- <el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button> -->
<!-- <el-button type="success">
<el-icon><Download /></el-icon>导出订单
</el-button> -->
</div> </div>
</el-card>
<!-- 订单列表 --> <!-- 订单列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="orderList" <div v-for="i in 5" :key="i" class="skeleton-row">
@selection-change="handleSelectionChange" <div class="skeleton-cell skeleton-checkbox"></div>
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-name"></div>
<el-table-column type="selection" width="55" /> <div class="skeleton-cell skeleton-user"></div>
<el-table-column prop="id" label="订单ID" width="100" /> <div class="skeleton-cell skeleton-price"></div>
<el-table-column prop="name" label="订单名称" min-width="180" /> <div class="skeleton-cell skeleton-status"></div>
<el-table-column prop="userId" label="用户ID" width="100" /> <div class="skeleton-cell skeleton-time"></div>
<el-table-column prop="commodityId" label="商品ID" width="100" /> <div class="skeleton-cell skeleton-action"></div>
<el-table-column label="表名" width="120"> </div>
<template #default="{ row }"> </div>
<el-tag size="small">{{ row.table || '未知' }}</el-tag>
</template> <el-table
</el-table-column> v-else
<el-table-column label="订单金额" width="120"> v-loading="loading"
<template #default="{ row }"> :data="orderList"
<span class="amount">¥{{ (row.price / 100).toFixed(2) }}</span> @selection-change="handleSelectionChange"
</template> style="width: 100%"
</el-table-column> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column label="续费价格" width="120"> >
<template #default="{ row }"> <el-table-column type="selection" width="55" />
<span class="renew-price">¥{{ (row.renewPrice / 100).toFixed(2) }}</span> <el-table-column prop="id" label="订单ID" width="100" />
</template> <el-table-column prop="name" label="订单名称" min-width="180" />
</el-table-column> <el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column label="数量" width="80"> <el-table-column prop="commodityId" label="商品ID" width="100" />
<template #default="{ row }"> <el-table-column label="表名" width="120">
<span>{{ row.payNum }}</span> <template #default="{ row }">
</template> <el-tag size="small">{{ row.table || '未知' }}</el-tag>
</el-table-column> </template>
<el-table-column label="订单状态" width="100"> </el-table-column>
<template #default="{ row }"> <el-table-column label="订单金额" width="120">
<el-tag :type="getStatusType(row.state)"> <template #default="{ row }">
{{ getStatusText(row.state) }} <span class="amount">¥{{ (row.price / 100).toFixed(2) }}</span>
</el-tag> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="续费价格" width="120">
<el-table-column label="支付方式" width="100"> <template #default="{ row }">
<template #default="{ row }"> <span class="renew-price">¥{{ (row.renewPrice / 100).toFixed(2) }}</span>
<span>{{ row.payType || '-' }}</span> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="数量" width="80">
<el-table-column label="过期时间" width="170"> <template #default="{ row }">
<template #default="{ row }"> <span>{{ row.payNum }}</span>
<span>{{ formatDate(row.expireTime) }}</span> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="订单状态" width="100">
<el-table-column label="创建时间" width="170"> <template #default="{ row }">
<template #default="{ row }"> <el-tag :type="getStatusType(row.state)">
<span>{{ formatDate(row.CreatedAt) }}</span> {{ getStatusText(row.state) }}
</template> </el-tag>
</el-table-column> </template>
<el-table-column label="操作" width="200" fixed="right"> </el-table-column>
<template #default="{ row }"> <el-table-column label="支付方式" width="100">
<el-button type="primary" link @click="handleView(row)">查看</el-button> <template #default="{ row }">
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button> <span>{{ row.payType || '-' }}</span>
<!-- <el-button type="danger" link @click="handleDelete(row)">删除</el-button> --> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="过期时间" width="170">
</el-table> <template #default="{ row }">
<span>{{ formatDate(row.expireTime) }}</span>
<!-- 分页 --> </template>
<el-pagination </el-table-column>
v-model:current-page="queryParams.page" <el-table-column label="创建时间" width="170">
v-model:page-size="queryParams.count" <template #default="{ row }">
:page-sizes="[10, 20, 50, 100]" <span>{{ formatDate(row.CreatedAt) }}</span>
layout="total, sizes, prev, pager, next, jumper" </template>
:total="total" </el-table-column>
@size-change="handleSizeChange" <el-table-column label="操作" width="200" fixed="right">
@current-change="handleCurrentChange" <template #default="{ row }">
background <div class="action-buttons">
class="pagination" <el-button type="primary" link @click="handleView(row)">查看</el-button>
/> <el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 订单详情对话框 --> <!-- 订单详情对话框 -->
@@ -133,6 +117,7 @@
v-model="detailDialogVisible" v-model="detailDialogVisible"
title="订单详情" title="订单详情"
width="800px" width="800px"
append-to-body
> >
<el-descriptions :column="2" border v-if="orderDetail"> <el-descriptions :column="2" border v-if="orderDetail">
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item> <el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
@@ -162,6 +147,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增订单' : '编辑订单'" :title="dialogType === 'add' ? '新增订单' : '编辑订单'"
width="700px" width="700px"
append-to-body
> >
<el-form <el-form
ref="orderFormRef" ref="orderFormRef"
@@ -217,8 +203,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -531,37 +519,40 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
} }
.search-form { .filter-section {
margin-bottom: 15px; padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
} }
.user-info { .action-buttons {
padding: 4px 0; display: flex;
} gap: 8px;
align-items: center;
.username {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
.user-id {
font-size: 12px;
color: #999;
} }
.amount { .amount {
@@ -577,8 +568,87 @@ onMounted(() => {
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-checkbox { width: 55px; }
.skeleton-id { width: 100px; }
.skeleton-name { width: 180px; }
.skeleton-user { width: 100px; }
.skeleton-price { width: 120px; }
.skeleton-status { width: 100px; }
.skeleton-time { width: 170px; }
.skeleton-action { width: 200px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+180 -59
View File
@@ -1,57 +1,77 @@
<template> <template>
<div class="product-group-container"> <div class="product-group-container">
<!-- 操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<div class="action-bar"> <!-- 操作栏 -->
<el-button type="primary" @click="handleAdd"> <div class="filter-section">
<el-icon><Plus /></el-icon>新增商品分组 <div class="filter-content">
</el-button> <div class="action-bar">
<el-button type="success" @click="fetchGroupList"> <el-button type="primary" @click="handleAdd">
<el-icon><Refresh /></el-icon> <el-icon><Plus /></el-icon>增商品分组
</el-button> </el-button>
<el-button type="success" @click="fetchGroupList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 商品分组列表 --> <!-- 商品分组列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="groupList" <div v-for="i in 5" :key="i" class="skeleton-row">
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-name"></div>
<el-table-column prop="id" label="分组ID" width="100" /> <div class="skeleton-cell skeleton-note"></div>
<el-table-column prop="name" label="分组名称" min-width="200" /> <div class="skeleton-cell skeleton-status"></div>
<el-table-column prop="note" label="备注" min-width="250" /> <div class="skeleton-cell skeleton-action"></div>
<el-table-column label="状态" width="100"> </div>
<template #default="{ row }"> </div>
<el-switch
v-model="row.disable" <el-table
:active-value="false" v-else
:inactive-value="true" v-loading="loading"
@change="(val) => handleStatusChange(row, val)" :data="groupList"
/> style="width: 100%"
</template> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
</el-table-column> >
<el-table-column label="操作" width="180" fixed="right"> <el-table-column prop="id" label="分组ID" width="100" />
<template #default="{ row }"> <el-table-column prop="name" label="分组名称" min-width="200" />
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-table-column label="状态" width="100">
</template> <template #default="{ row }">
</el-table-column> <el-switch
</el-table> v-model="row.disable"
:active-value="false"
<!-- 分页 --> :inactive-value="true"
<el-pagination @change="(val) => handleStatusChange(row, val)"
v-model:current-page="queryParams.page" />
v-model:page-size="queryParams.count" </template>
:page-sizes="[10, 20, 50, 100]" </el-table-column>
layout="total, sizes, prev, pager, next, jumper" <el-table-column label="操作" width="180" fixed="right">
:total="total" <template #default="{ row }">
@size-change="handleSizeChange" <div class="action-buttons">
@current-change="handleCurrentChange" <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
background <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
class="pagination" </div>
/> </template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 商品分组表单对话框 --> <!-- 商品分组表单对话框 -->
@@ -59,6 +79,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品分组' : '编辑商品分组'" :title="dialogType === 'add' ? '新增商品分组' : '编辑商品分组'"
width="600px" width="600px"
append-to-body
> >
<el-form <el-form
ref="groupFormRef" ref="groupFormRef"
@@ -80,8 +101,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -253,23 +276,121 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-id { width: 100px; }
.skeleton-name { width: 200px; }
.skeleton-note { flex: 1; min-width: 250px; }
.skeleton-status { width: 100px; }
.skeleton-action { width: 180px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+653 -109
View File
@@ -1,101 +1,123 @@
<template> <template>
<div class="product-list-container"> <div class="product-list-container">
<!-- 搜索和操作栏 --> <!-- 搜索和商品列表 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="商品分组"> <div class="filter-section">
<el-select v-model="queryParams.good_group_id" placeholder="请选择分组" clearable style="width: 200px"> <div class="filter-content">
<el-option <el-form :inline="true" :model="queryParams" class="search-form">
v-for="item in groupOptions" <el-form-item label="商品分组">
:key="item.id" <el-select v-model="queryParams.good_group_id" placeholder="请选择分组" clearable style="width: 200px">
:label="item.name" <el-option
:value="item.id" v-for="item in groupOptions"
/> :key="item.id"
</el-select> :label="item.name"
</el-form-item> :value="item.id"
<el-form-item> />
<el-button type="primary" @click="handleQuery"> </el-select>
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleAdd"> <el-button type="success" @click="fetchProductList">
<el-icon><Plus /></el-icon>增商品 <el-icon><Refresh /></el-icon>
</el-button> </el-button>
<el-button type="success" @click="fetchProductList"> </el-form-item>
<el-icon><Refresh /></el-icon>刷新 </el-form>
</el-button> <div class="action-bar">
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="primary" @click="handleAdd">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Plus /></el-icon>新增商品
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 商品列表 --> <!-- 商品列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="productList" <div v-for="i in 5" :key="i" class="skeleton-row">
@selection-change="handleSelectionChange" <div class="skeleton-cell skeleton-checkbox"></div>
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-image"></div>
<el-table-column type="selection" width="55" /> <div class="skeleton-cell skeleton-name"></div>
<el-table-column prop="id" label="商品ID" width="100" /> <div class="skeleton-cell skeleton-table"></div>
<el-table-column label="商品图片" width="100"> <div class="skeleton-cell skeleton-price"></div>
<template #default="{ row }"> <div class="skeleton-cell skeleton-tag"></div>
<el-image <div class="skeleton-cell skeleton-inventory"></div>
:src="row.image || '/logo.svg'" <div class="skeleton-cell skeleton-action"></div>
fit="cover" </div>
style="width: 60px; height: 60px; border-radius: 4px" </div>
/>
</template> <el-table
</el-table-column> v-else
<el-table-column prop="name" label="商品名称" min-width="200" /> v-loading="loading"
<el-table-column prop="table" label="商品所属表" width="150" /> :data="productList"
<el-table-column label="价格" width="120"> @selection-change="handleSelectionChange"
<template #default="{ row }"> style="width: 100%"
<span class="price">¥{{ (row.price / 100).toFixed(2) }}</span> :header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600 }"
</template> >
</el-table-column> <el-table-column type="selection" width="55" />
<el-table-column label="库存控制" width="100"> <el-table-column prop="id" label="商品ID" width="100" />
<template #default="{ row }"> <el-table-column label="商品图片" width="100">
<el-tag :type="row.inventory_control ? 'success' : 'info'"> <template #default="{ row }">
{{ row.inventory_control ? '已启用' : '未启用' }} <el-image
</el-tag> :src="row.image || '/logo.svg'"
</template> fit="cover"
</el-table-column> style="width: 60px; height: 60px"
<el-table-column prop="inventory" label="库存" width="100" /> />
<el-table-column prop="payNum" label="单次数量" width="100" /> </template>
<el-table-column label="推荐" width="80"> </el-table-column>
<template #default="{ row }"> <el-table-column prop="name" label="商品名称" min-width="200" />
<el-tag :type="row.recommend ? 'success' : 'info'" size="small"> <el-table-column prop="table" label="商品所属表" width="150" />
{{ row.recommend ? '是' : '否' }} <el-table-column label="价格" width="120">
</el-tag> <template #default="{ row }">
</template> <span class="price">¥{{ (row.price / 100).toFixed(2) }}</span>
</el-table-column> </template>
<el-table-column label="操作" width="200" fixed="right"> </el-table-column>
<template #default="{ row }"> <el-table-column label="库存控制" width="100">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <template #default="{ row }">
<!-- <el-button type="warning" link @click="handleSpec(row)">规格</el-button> --> <el-tag :type="row.inventoryControl ? 'success' : 'info'">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> {{ row.inventoryControl ? '已启用' : '未启用' }}
</template> </el-tag>
</el-table-column> </template>
</el-table> </el-table-column>
<el-table-column prop="inventory" label="库存" width="100" />
<!-- 分页 --> <el-table-column prop="payNum" label="单次数量" width="100" />
<el-pagination <el-table-column label="推荐" width="80">
v-model:current-page="queryParams.page" <template #default="{ row }">
v-model:page-size="queryParams.count" <el-tag :type="row.recommend ? 'success' : 'info'" size="small">
:page-sizes="[10, 20, 50, 100]" {{ row.recommend ? '是' : '否' }}
layout="total, sizes, prev, pager, next, jumper" </el-tag>
:total="total" </template>
@size-change="handleSizeChange" </el-table-column>
@current-change="handleCurrentChange" <el-table-column label="操作" width="200" fixed="right">
background <template #default="{ row }">
class="pagination" <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
/> <el-button type="warning" link @click="handleParameter(row)">参数</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 商品表单对话框 --> <!-- 商品表单对话框 -->
@@ -103,6 +125,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品' : '编辑商品'" :title="dialogType === 'add' ? '新增商品' : '编辑商品'"
width="700px" width="700px"
style="margin-top: 300px;"
> >
<el-form <el-form
ref="productFormRef" ref="productFormRef"
@@ -132,8 +155,8 @@
<el-form-item label="封面ID" prop="cover_id"> <el-form-item label="封面ID" prop="cover_id">
<el-input-number v-model="productForm.cover_id" :min="0" placeholder="请输入封面ID" style="width: 100%" /> <el-input-number v-model="productForm.cover_id" :min="0" placeholder="请输入封面ID" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item label="库存控制" prop="inventory_control"> <el-form-item label="库存控制" prop="inventoryControl">
<el-switch v-model="productForm.inventory_control" active-text="启用" inactive-text="禁用" /> <el-switch v-model="productForm.inventoryControl" active-text="启用" inactive-text="禁用" />
</el-form-item> </el-form-item>
<el-form-item label="库存数量" prop="inventory"> <el-form-item label="库存数量" prop="inventory">
<el-input-number v-model="productForm.inventory" :min="0" placeholder="请输入库存" style="width: 100%" /> <el-input-number v-model="productForm.inventory" :min="0" placeholder="请输入库存" style="width: 100%" />
@@ -159,6 +182,152 @@
<el-button type="primary" @click="submitForm">确定</el-button> <el-button type="primary" @click="submitForm">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 商品参数列表对话框 -->
<el-dialog
v-model="paramDialogVisible"
title="商品参数管理"
width="900px"
>
<div class="filter-section" style="border: none; padding: 0 0 16px 0;">
<div class="action-bar">
<el-button type="primary" @click="handleAddParameter">
<el-icon><Plus /></el-icon>新增参数
</el-button>
<el-button type="success" @click="fetchParameterList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
<el-table
v-loading="paramLoading"
:data="parameterList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="参数ID" width="100" />
<el-table-column prop="name" label="参数名称" min-width="150" />
<el-table-column prop="type" label="参数类型" width="120">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">
{{ getArgTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParameter(row)">编辑</el-button>
<el-button type="success" link @click="handleViewParamValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDeleteParameter(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 商品参数表单对话框 -->
<el-dialog
v-model="paramFormDialogVisible"
:title="paramFormType === 'add' ? '新增商品参数' : '编辑商品参数'"
width="600px"
append-to-body
>
<el-form
ref="paramFormRef"
:model="paramForm"
:rules="paramRules"
label-width="100px"
>
<el-form-item label="参数名称" prop="arg_name">
<el-input v-model="paramForm.arg_name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-radio-group v-model="paramForm.arg_type">
<el-radio label="string">字符串</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio label="select">选择</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="paramFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitParamForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog
v-model="paramValuesDialogVisible"
title="参数值管理"
width="800px"
append-to-body
>
<div class="values-header">
<span>参数{{ currentParam?.name }}</span>
<el-button type="primary" @click="handleAddParamValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="paramValuesLoading"
:data="paramValueList"
style="width: 100%; margin-top: 20px"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column prop="id" label="值ID" width="100" />
<el-table-column prop="name" label="值名称" min-width="150" />
<el-table-column prop="value" label="值" min-width="150" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
¥{{ (row.price / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEditParamValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteParamValue(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="paramValueFormDialogVisible"
:title="paramValueFormType === 'add' ? '添加参数值' : '编辑参数值'"
width="500px"
append-to-body
>
<el-form
ref="paramValueFormRef"
:model="paramValueForm"
:rules="paramValueRules"
label-width="100px"
>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="paramValueForm.attr_name" placeholder="请输入值名称" />
</el-form-item>
<el-form-item label="值" prop="attr_value">
<el-input v-model="paramValueForm.attr_value" placeholder="请输入值" />
</el-form-item>
<el-form-item label="价格(元)" prop="attr_price">
<el-input-number v-model="paramValueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="paramValueFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitParamValueForm">确定</el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -167,8 +336,16 @@ import { ref, reactive, onMounted } from 'vue'
import { getFileDetail } from '@/api/admin/file' import { getFileDetail } from '@/api/admin/file'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue' import { Plus, Delete, Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, createProduct, updateProduct, deleteProduct } from '@/api/admin/product' import { getProductList, createProduct, updateProduct, deleteProduct, getProductGroupList,
import { getProductGroupList } from '@/api/admin/product' getProductParameterList,
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue
} from '@/api/admin/product'
// //
const queryParams = reactive({ const queryParams = reactive({
@@ -322,6 +499,7 @@ const handleAdd = () => {
// //
const handleEdit = (row) => { const handleEdit = (row) => {
console.log("更新:",row)
dialogType.value = 'edit' dialogType.value = 'edit'
dialogVisible.value = true dialogVisible.value = true
Object.assign(productForm, { Object.assign(productForm, {
@@ -331,7 +509,7 @@ const handleEdit = (row) => {
content: row.content, content: row.content,
cover_id: row.coverId, cover_id: row.coverId,
good_group_id: row.goodGroupId, good_group_id: row.goodGroupId,
inventory_control: row.inventory_control, inventory_control: row.inventoryControl,
inventory: row.inventory, inventory: row.inventory,
price: row.price, price: row.price,
pay_num: row.payNum, pay_num: row.payNum,
@@ -427,10 +605,12 @@ const submitForm = () => {
good_group_id: Number(productForm.good_group_id), // good_group_id: Number(productForm.good_group_id), //
cover_id: productForm.cover_id || 0, cover_id: productForm.cover_id || 0,
inventory: productForm.inventory || 0, inventory: productForm.inventory || 0,
price: productForm.price || 0, price: productForm.price/100 || 0,
pay_num: productForm.pay_num || 1, pay_num: productForm.pay_num || 1,
expire_time: productForm.expire_time || 0, expire_time: productForm.expire_time || 0,
recommend_rebate: productForm.recommend_rebate || 0 recommend_rebate: productForm.recommend_rebate || 0,
inventory_control:productForm.inventoryControl
} }
console.log('提交的数据:', submitData) // console.log('提交的数据:', submitData) //
@@ -458,6 +638,259 @@ onMounted(() => {
fetchProductList() fetchProductList()
fetchGroupList() fetchGroupList()
}) })
// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------
//
const paramDialogVisible = ref(false)
const paramLoading = ref(false)
const parameterList = ref([])
const currentProductId = ref(null)
const paramFormDialogVisible = ref(false)
const paramFormType = ref('add')
const paramFormRef = ref(null)
const paramForm = reactive({
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
const paramRules = {
arg_name: [{ required: true, message: '请输入参数名称', trigger: 'blur' }],
arg_type: [{ required: true, message: '请选择参数类型', trigger: 'change' }]
}
const paramValuesDialogVisible = ref(false)
const paramValuesLoading = ref(false)
const paramValueList = ref([])
const currentParam = ref(null)
const paramValueFormDialogVisible = ref(false)
const paramValueFormType = ref('add')
const paramValueFormRef = ref(null)
const paramValueForm = reactive({
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
const paramValueRules = {
attr_name: [{ required: true, message: '请输入值名称', trigger: 'blur' }],
attr_value: [{ required: true, message: '请输入值', trigger: 'blur' }],
attr_price: [{ required: true, message: '请输入价格', trigger: 'blur' }]
}
//
const handleParameter = (row) => {
currentProductId.value = row.id
paramDialogVisible.value = true
fetchParameterList()
}
//
const fetchParameterList = async () => {
if (!currentProductId.value) return
paramLoading.value = true
try {
const res = await getProductParameterList({ good_id: currentProductId.value })
if (res.data.code === 200) {
parameterList.value = res.data.data || []
}
} catch (error) {
ElMessage.error('获取参数列表失败')
} finally {
paramLoading.value = false
}
}
//
const getArgTypeText = (type) => {
const typeMap = { 'string': '字符串', 'number': '数字', 'select': '选择' }
return typeMap[type] || '未知'
}
const getArgTypeTag = (type) => {
const tagMap = { 'string': 'primary', 'number': 'success', 'select': 'warning' }
return tagMap[type] || 'info'
}
//
const handleAddParameter = () => {
paramFormType.value = 'add'
paramFormDialogVisible.value = true
Object.assign(paramForm, {
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
paramFormRef.value?.resetFields()
}
//
const handleEditParameter = (row) => {
paramFormType.value = 'edit'
paramFormDialogVisible.value = true
Object.assign(paramForm, {
arg_id: row.id,
arg_name: row.name,
arg_type: row.type
})
}
//
const handleDeleteParameter = (row) => {
ElMessageBox.confirm(`确认删除参数 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameter({ good_id: currentProductId.value, arg_id: row.id })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParameterList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
//
const submitParamForm = () => {
paramFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(currentProductId.value),
arg_name: paramForm.arg_name,
arg_type: paramForm.arg_type
}
if (paramFormType.value === 'edit') {
submitData.arg_id = paramForm.arg_id
}
const res = paramFormType.value === 'add'
? await createProductParameter(submitData)
: await updateProductParameter(submitData)
if (res.data.code === 200) {
ElMessage.success(paramFormType.value === 'add' ? '新增成功' : '修改成功')
paramFormDialogVisible.value = false
fetchParameterList()
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
//
const handleViewParamValues = (row) => {
currentParam.value = row
paramValuesDialogVisible.value = true
fetchParamValuesList()
}
//
const fetchParamValuesList = async () => {
if (!currentProductId.value || !currentParam.value) return
paramValuesLoading.value = true
try {
const res = await getProductParameterDetail({
good_id: currentProductId.value,
arg_id: currentParam.value.id
})
if (res.data.code === 200) {
paramValueList.value = res.data.data.attrs || []
}
} catch (error) {
ElMessage.error('获取参数值列表失败')
} finally {
paramValuesLoading.value = false
}
}
//
const handleAddParamValue = () => {
paramValueFormType.value = 'add'
paramValueFormDialogVisible.value = true
Object.assign(paramValueForm, {
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
paramValueFormRef.value?.resetFields()
}
//
const handleEditParamValue = (row) => {
paramValueFormType.value = 'edit'
paramValueFormDialogVisible.value = true
Object.assign(paramValueForm, {
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100
})
}
//
const handleDeleteParamValue = (row) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({
good_id: currentProductId.value,
attr_id: row.id
})
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParamValuesList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
//
const submitParamValueForm = () => {
paramValueFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(currentProductId.value),
arg_id: Number(currentParam.value.id),
attr_name: paramValueForm.attr_name,
attr_value: paramValueForm.attr_value,
attr_price: paramValueForm.attr_price
}
if (paramValueFormType.value === 'edit') {
submitData.attr_id = paramValueForm.attr_id
}
const res = paramValueFormType.value === 'add'
? await addProductParameterValue(submitData)
: await updateProductParameterValue(submitData)
if (res.data.code === 200) {
ElMessage.success(paramValueFormType.value === 'add' ? '添加成功' : '修改成功')
paramValueFormDialogVisible.value = false
fetchParamValuesList()
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
</script> </script>
<style scoped> <style scoped>
@@ -465,33 +898,144 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .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 { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { @media (max-width: 768px) {
border-radius: 8px; .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;
} }
.price { .price {
color: #f56c6c; color: #e74c3c;
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 14px;
} }
.pagination { .pagination {
margin-top: 24px; padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
:deep(.el-card__body) {
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 {
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;
border-radius: 4px;
}
.skeleton-checkbox { width: 55px; }
.skeleton-id { width: 100px; }
.skeleton-image { width: 60px; height: 60px; }
.skeleton-name { width: 200px; }
.skeleton-table { width: 150px; }
.skeleton-price { width: 120px; }
.skeleton-tag { width: 100px; }
.skeleton-inventory { width: 100px; }
.skeleton-action { width: 200px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.values-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style> </style>
-637
View File
@@ -1,637 +0,0 @@
<template>
<div class="product-parameter-container">
<!-- 操作栏 -->
<el-card class="filter-container" shadow="never">
<el-form ref="queryFormRef" label-width="100px" :inline="true" :model="queryParams" class="search-form">
<el-form-item label="商品分组">
<el-select
v-model="queryParams.good_group_id"
placeholder="请选择商品分组"
clearable
@change="handleGroupChange"
style="width: 200px"
>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="商品">
<el-select
v-model="queryParams.good_id"
placeholder="请先选择商品分组"
clearable
:disabled="!queryParams.good_group_id"
style="width: 200px"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</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-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增商品参数
</el-button>
<el-button type="success" @click="fetchParameterList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</el-card>
<!-- 商品参数列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="parameterList"
style="width: 100%"
>
<el-table-column prop="id" label="参数ID" width="100" />
<el-table-column prop="name" label="参数名称" min-width="200" />
<el-table-column prop="type" label="参数类型" width="120">
<template #default="{ row }">
<el-tag :type="getArgTypeTag(row.type)">
{{ getArgTypeText(row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleViewValues(row)">查看参数值</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 商品参数表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增商品参数' : '编辑商品参数'"
width="600px"
>
<el-form
ref="parameterFormRef"
:model="parameterForm"
:rules="parameterRules"
label-width="100px"
>
<el-form-item label="参数名称" prop="arg_name">
<el-input v-model="parameterForm.arg_name" placeholder="请输入参数名称" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-radio-group v-model="parameterForm.arg_type">
<el-radio label="string">字符串</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio label="select">选择</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 参数值管理对话框 -->
<el-dialog
v-model="valuesDialogVisible"
title="参数值管理"
width="800px"
>
<div class="values-header">
<span>参数{{ currentParameter?.arg_name }}</span>
<el-button type="primary" @click="handleAddValue">
<el-icon><Plus /></el-icon>添加参数值
</el-button>
</div>
<el-table
v-loading="valuesLoading"
:data="valuesList"
style="width: 100%; margin-top: 20px"
>
<el-table-column prop="id" label="值ID" width="100" />
<el-table-column prop="name" label="值名称" min-width="150" />
<el-table-column prop="value" label="值" min-width="150" />
<el-table-column label="价格" width="120">
<template #default="{ row }">
¥{{ (row.price / 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEditValue(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteValue(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 参数值表单对话框 -->
<el-dialog
v-model="valueDialogVisible"
:title="valueDialogType === 'add' ? '添加参数值' : '编辑参数值'"
width="500px"
>
<el-form
ref="valueFormRef"
:model="valueForm"
:rules="valueRules"
label-width="100px"
>
<el-form-item label="值名称" prop="attr_name">
<el-input v-model="valueForm.attr_name" placeholder="请输入值名称" />
</el-form-item>
<el-form-item label="值" prop="attr_value">
<el-input v-model="valueForm.attr_value" placeholder="请输入值" />
</el-form-item>
<el-form-item label="价格(元)" prop="attr_price">
<el-input-number v-model="valueForm.attr_price" :min="0" :precision="2" :step="0.01" placeholder="请输入价格" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="valueDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitValueForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import {
getProductParameterList,
getProductParameterDetail,
createProductParameter,
updateProductParameter,
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue,
getProductList,
getProductGroupList
} from '@/api/admin/product'
//
const queryParams = reactive({
good_group_id: undefined, // ID
good_id: undefined, // ID
page: 1,
count: 10
})
//
const groupOptions = ref([]) //
const productOptions = ref([]) //
const queryFormRef = ref(null)
//
const parameterForm = reactive({
good_id: undefined,
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
const parameterRules = {
arg_name: [
{ required: true, message: '请输入参数名称', trigger: 'blur' }
],
arg_type: [
{ required: true, message: '请选择参数类型', trigger: 'change' }
]
}
//
const valueForm = reactive({
good_id: undefined,
arg_id: undefined,
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
const valueRules = {
attr_name: [
{ required: true, message: '请输入值名称', trigger: 'blur' }
],
attr_value: [
{ required: true, message: '请输入值', trigger: 'blur' }
],
attr_price: [
{ required: true, message: '请输入价格', trigger: 'blur' }
]
}
//
const loading = ref(false)
const valuesLoading = ref(false)
const parameterList = ref([])
const valuesList = ref([])
const total = ref(0)
const dialogVisible = ref(false)
const valuesDialogVisible = ref(false)
const valueDialogVisible = ref(false)
const dialogType = ref('add')
const valueDialogType = ref('add')
const currentParameter = ref(null)
const parameterFormRef = ref(null)
const valueFormRef = ref(null)
//
const fetchGroupList = async () => {
try {
const res = await getProductGroupList({ page: 1, count: 100 })
console.log('商品分组列表:', res.data)
if (res.data.code === 200) {
groupOptions.value = res.data.data.data || []
}
} catch (error) {
console.error('获取商品分组列表失败:', error)
ElMessage.error('获取商品分组列表失败')
}
}
// ID
const fetchProductList = async (groupId) => {
try {
const res = await getProductList({ good_group_id: groupId, page: 1, count: 100 })
console.log('商品列表:', res.data)
if (res.data.code === 200) {
productOptions.value = res.data.data.data || []
productOptions.value = productOptions.value.filter(item => item.delete == false)
}
} catch (error) {
console.error('获取商品列表失败:', error)
ElMessage.error('获取商品列表失败')
}
}
//
const handleGroupChange = (groupId) => {
//
queryParams.good_id = undefined
productOptions.value = []
if (groupId) {
//
fetchProductList(groupId)
}
}
//
const fetchParameterList = async () => {
// ID
if (!queryParams.good_id) {
ElMessage.warning('请先选择商品')
return
}
loading.value = true
try {
const res = await getProductParameterList({ good_id: queryParams.good_id })
console.log('商品参数列表:', res.data)
if (res.data.code === 200) {
parameterList.value = res.data.data || []
total.value = res.data.data.length || 0
}
} catch (error) {
console.error('获取商品参数列表失败:', error)
ElMessage.error('获取商品参数列表失败')
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
queryParams.page = 1
fetchParameterList()
}
//
const resetQuery = () => {
queryParams.good_group_id = undefined
queryParams.good_id = undefined
queryParams.page = 1
productOptions.value = []
parameterList.value = []
total.value = 0
}
//
const fetchValuesList = async (goodId, argId) => {
valuesLoading.value = true
console.log('goodId', goodId)
console.log('argId', argId)
try {
const res = await getProductParameterDetail({ good_id: goodId, arg_id: argId })
console.log('参数值列表:', res.data)
if (res.data.code === 200) {
valuesList.value = res.data.data.attrs || []
}
} catch (error) {
console.error('获取参数值列表失败:', error)
ElMessage.error('获取参数值列表失败')
} finally {
valuesLoading.value = false
}
}
//
const getArgTypeText = (type) => {
const typeMap = {
'string': '字符串',
'number': '数字',
'select': '选择'
}
return typeMap[type] || '未知'
}
//
const getArgTypeTag = (type) => {
const tagMap = {
'string': 'primary',
'number': 'success',
'select': 'warning'
}
return tagMap[type] || 'info'
}
//
const handleSizeChange = (size) => {
queryParams.count = size
fetchParameterList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchParameterList()
}
//
const handleAdd = () => {
if (!queryParams.good_id) {
ElMessage.warning('请先选择商品')
return
}
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(parameterForm, {
good_id: queryParams.good_id,
arg_id: undefined,
arg_name: '',
arg_type: 'string'
})
parameterFormRef.value?.resetFields()
}
//
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
Object.assign(parameterForm, {
good_id: queryParams.good_id,
arg_id: row.id,
arg_name: row.name,
arg_type: row.type
})
}
//
const handleViewValues = (row) => {
currentParameter.value = row
valuesDialogVisible.value = true
fetchValuesList(queryParams.good_id, row.id)
}
//
const handleAddValue = () => {
valueDialogType.value = 'add'
console.log('currentParameter', currentParameter.value)
valueDialogVisible.value = true
Object.assign(valueForm, {
good_id: queryParams.good_id,
arg_id: currentParameter.value.id,
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
})
valueFormRef.value?.resetFields()
}
//
const handleEditValue = (row) => {
valueDialogType.value = 'edit'
valueDialogVisible.value = true
Object.assign(valueForm, {
good_id: queryParams.good_id,
arg_id: currentParameter.value.id,
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100 //
})
}
//
const handleDeleteValue = (row) => {
ElMessageBox.confirm(`确认删除参数值 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameterValue({
good_id: queryParams.good_id,
attr_id: row.id
})
console.log('删除参数值响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchValuesList(queryParams.good_id, currentParameter.value.id)
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除商品参数 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteProductParameter({
good_id: queryParams.good_id,
arg_id: row.id
})
console.log('删除参数响应:', res.data)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchParameterList()
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.response?.data?.message || '删除失败')
}
}).catch(() => {})
}
//
const submitForm = () => {
parameterFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(parameterForm.good_id),
arg_name: parameterForm.arg_name,
arg_type: parameterForm.arg_type
}
if (dialogType.value === 'edit') {
submitData.arg_id = parameterForm.arg_id
}
console.log('提交参数数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await createProductParameter(submitData)
} else {
res = await updateProductParameter(submitData)
}
console.log('提交参数响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchParameterList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
//
const submitValueForm = () => {
valueFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: Number(valueForm.good_id),
arg_id: Number(valueForm.arg_id),
attr_name: valueForm.attr_name,
attr_value: valueForm.attr_value,
attr_price: valueForm.attr_price //
}
if (valueDialogType.value === 'edit') {
submitData.attr_id = valueForm.attr_id
}
console.log('提交参数值数据:', submitData)
let res
if (valueDialogType.value === 'add') {
res = await addProductParameterValue(submitData)
} else {
res = await updateProductParameterValue(submitData)
}
console.log('提交参数值响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(valueDialogType.value === 'add' ? '添加成功' : '修改成功')
valueDialogVisible.value = false
fetchValuesList(queryParams.good_id, currentParameter.value.id)
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
//
onMounted(() => {
//
fetchGroupList()
})
</script>
<style scoped>
.product-parameter-container {
padding: 0;
}
.filter-container {
margin-bottom: 20px;
border-radius: 8px;
}
.search-form {
margin-bottom: 15px;
}
.action-bar {
display: flex;
gap: 12px;
}
.table-container {
border-radius: 8px;
}
.values-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
</style>
+146 -127
View File
@@ -1,77 +1,76 @@
<template> <template>
<div class="global-setting-container"> <div class="global-setting-container">
<!-- 页面头部 --> <el-card class="main-container" shadow="never">
<div class="page-header"> <!-- 搜索筛选 -->
<div class="left"> <div class="filter-section">
<h2 class="title">全局设置</h2> <div class="filter-content">
<el-tag type="info" effect="plain" class="info-tag">系统全局配置管理</el-tag> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="设置名称">
<el-input v-model="queryParams.name" placeholder="请输入设置名称" clearable />
</el-form-item>
<el-form-item label="权限">
<el-select v-model="queryParams.authority" placeholder="请选择权限" clearable style="width: 120px">
<el-option label="全部" value="" />
<el-option label="公有" value="0" />
<el-option label="私有" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button @click="resetQuery">
<el-icon><Delete /></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 @click="handleRefresh">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div> </div>
<div class="actions">
<el-button type="primary" @click="handleAdd" :icon="Plus" class="action-btn">
新增设置
</el-button>
<el-button type="success" @click="handleRefresh" :icon="Refresh" class="action-btn">
刷新
</el-button>
</div>
</div>
<!-- 搜索筛选 --> <!-- 设置列表 -->
<!-- <el-card class="filter-container" shadow="never"> <div class="table-section">
<el-form :inline="true" :model="queryParams" class="search-form"> <el-table
<el-form-item label="设置名称"> v-loading="loading"
<el-input v-model="queryParams.name" placeholder="请输入设置名称" clearable /> :data="settingsList"
</el-form-item> style="width: 100%"
<el-form-item label="权限"> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-select v-model="queryParams.authority" placeholder="请选择权限" clearable> >
<el-option label="全部" value="" /> <el-table-column prop="setting_id" label="ID" width="80" />
<el-option label="公有" value="0" /> <el-table-column prop="name" label="Name值" min-width="200" show-overflow-tooltip />
<el-option label="私有" value="1" /> <el-table-column prop="value" label="Value值" min-width="150" show-overflow-tooltip />
</el-select> <el-table-column label="权限" width="100" align="center">
</el-form-item> <template #default="{ row }">
<el-form-item> <el-tag :type="getAuthorityType(row.authority)" size="small">
<el-button type="primary" @click="handleQuery" :icon="Search">查询</el-button> {{ getAuthorityText(row.authority) }}
<el-button @click="resetQuery" :icon="Delete">重置</el-button> </el-tag>
</el-form-item> </template>
</el-form> </el-table-column>
</el-card> --> <el-table-column prop="notes" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="180" />
<!-- 设置列表 --> <el-table-column label="操作" width="150" fixed="right" align="center">
<el-card class="table-container" shadow="never"> <template #default="{ row }">
<el-table
v-loading="loading"
:data="settingsList"
style="width: 100%"
border
stripe
>
<el-table-column prop="setting_id" label="ID" width="80" />
<el-table-column prop="name" label="Name值" min-width="200" show-overflow-tooltip />
<el-table-column prop="value" label="Value值" min-width="150" show-overflow-tooltip />
<el-table-column label="权限" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getAuthorityType(row.authority)" size="small">
{{ getAuthorityText(row.authority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="notes" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<div class="action-buttons">
<el-tooltip content="编辑" placement="top"> <el-tooltip content="编辑" placement="top">
<el-button type="primary" :icon="Edit" circle size="small" @click="handleEdit(row)" /> <el-button type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<el-button type="danger" :icon="Delete" circle size="small" @click="handleDelete(row)" /> <el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</el-tooltip> </el-tooltip>
</div> </template>
</template> </el-table-column>
</el-table-column> </el-table>
</el-table> </div>
</el-card> </el-card>
<!-- 新增/编辑设置对话框 --> <!-- 新增/编辑设置对话框 -->
@@ -256,13 +255,17 @@ const getAuthorityText = (authority) => {
return authority === 0 ? '公有' : '私有' return authority === 0 ? '公有' : '私有'
} }
//
const handleQuery = () => {
getList()
}
//
const resetQuery = () => {
queryParams.name = ''
queryParams.authority = ''
getList()
}
// //
const handleRefresh = () => { const handleRefresh = () => {
@@ -387,65 +390,83 @@ onMounted(() => {
<style scoped> <style scoped>
.global-setting-container { .global-setting-container {
padding: 20px; padding: 0;
min-height: calc(100vh - 120px);
background-color: #f5f7fa;
} }
/* 页面标题样式 */ .main-container {
.page-header { border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 24px; padding: 16px 20px;
padding-bottom: 16px; gap: 20px;
border-bottom: 1px solid #ebeef5; flex-wrap: wrap;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.info-tag {
font-size: 13px;
}
.page-header .actions {
display: flex;
gap: 12px;
align-items: center;
}
/* 筛选容器 */
.filter-container {
margin-bottom: 20px;
} }
.search-form { .search-form {
margin-bottom: 0; margin: 0;
} flex: 1;
/* 表格容器 */
.table-container {
margin-bottom: 20px;
}
.action-buttons {
display: flex; display: flex;
justify-content: center; align-items: center;
gap: 8px; gap: 12px;
flex-wrap: wrap;
} }
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 对话框底部 */ /* 对话框底部 */
.dialog-footer { .dialog-footer {
@@ -455,20 +476,18 @@ onMounted(() => {
/* 响应式设计 */ /* 响应式设计 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.page-header { .filter-content {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: stretch;
gap: 16px;
} }
.page-header .actions { .search-form {
width: 100%; width: 100%;
flex-wrap: wrap;
} }
.action-buttons { .action-bar {
flex-direction: column; width: 100%;
gap: 4px; justify-content: flex-start;
} }
} }
</style> </style>
+136 -72
View File
@@ -1,67 +1,73 @@
<template> <template>
<div class="domain-whitelist-container"> <div class="domain-whitelist-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="域名"> <div class="filter-section">
<el-input v-model="queryParams.domain" placeholder="请输入域名" clearable /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item> <el-form-item label="域名">
<el-button type="primary" @click="handleQuery">查询</el-button> <el-input v-model="queryParams.domain" placeholder="请输入域名" clearable />
<el-button @click="resetQuery">重置</el-button> </el-form-item>
</el-form-item> <el-form-item>
</el-form> <el-button type="primary" @click="handleQuery">
<div class="action-bar"> <el-icon><Search /></el-icon>查询
<el-button type="primary" @click="handleAdd"> </el-button>
<el-icon><plus /></el-icon>新增域名 <el-button @click="resetQuery">重置</el-button>
</el-button> </el-form-item>
</el-form>
<el-button type="success" @click="getList"> <div class="action-bar">
<el-icon><Refresh /></el-icon>刷新 <el-button type="primary" @click="handleAdd">
</el-button> <el-icon><Plus /></el-icon>新增域名
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="success" @click="getList">
<el-icon><delete /></el-icon>批量删除 <el-icon><Refresh /></el-icon>刷新
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 域名列表 --> <!-- 域名列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="domainList" :data="domainList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column type="selection" width="55" /> >
<el-table-column prop="id" label="ID" width="80" /> <el-table-column type="selection" width="55" />
<el-table-column prop="domain" label="域名" min-width="200" > <el-table-column prop="id" label="ID" width="80" />
<template #default="{ row }"> <el-table-column prop="domain" label="域名" min-width="200" >
<el-link :href="`http://${row.domain}`" target="_blank" type="primary">{{ row.domain }}</el-link> <template #default="{ row }">
</template> <el-link :href="`http://${row.domain}`" target="_blank" type="primary">{{ row.domain }}</el-link>
</el-table-column> </template>
<el-table-column prop="CreatedAt" label="创建时间" width="180" :formatter="parseCreatedAt" /> </el-table-column>
<el-table-column prop="UpdatedAt" label="更新时间" width="180" :formatter="parseUpdatedAt" /> <el-table-column prop="CreatedAt" label="创建时间" width="180" :formatter="parseCreatedAt" />
<el-table-column label="操作" width="100" fixed="right"> <el-table-column prop="UpdatedAt" label="更新时间" width="180" :formatter="parseUpdatedAt" />
<template #default="{ row }"> <el-table-column label="操作" width="100" fixed="right">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <template #default="{ row }">
</template> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</el-table-column> </template>
</el-table> </el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <!-- 分页 -->
v-model:current-page="queryParams.pageNum" <el-pagination
v-model:page-size="queryParams.pageSize" v-model:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]" v-model:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"
:total="total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" :total="total"
@current-change="handleCurrentChange" @size-change="handleSizeChange"
background @current-change="handleCurrentChange"
class="pagination" background
/> class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 域名表单对话框 --> <!-- 域名表单对话框 -->
@@ -94,7 +100,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete } from '@element-plus/icons-vue' import { Plus, Delete, Refresh, Search } from '@element-plus/icons-vue'
import { getDomainList, addDomain, deleteDomain, batchDeleteDomain } from '@/api/domain' import { getDomainList, addDomain, deleteDomain, batchDeleteDomain } from '@/api/domain'
// //
@@ -284,35 +290,93 @@ onMounted(() => {
<style scoped> <style scoped>
.domain-whitelist-container { .domain-whitelist-container {
padding: 10px; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; 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 { .search-form {
margin-bottom: 10px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
} }
.action-bar { .action-bar {
display: flex; display: flex;
justify-content: flex-start; gap: 12px;
margin-bottom: 10px; flex-shrink: 0;
} }
.table-container { .table-section {
margin-bottom: 20px; padding: 0;
} }
.pagination { .pagination {
margin-top: 15px; margin-top: 20px;
display: flex; padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 12px;
} }
</style>
/* 表格样式优化 */
: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>
+148 -165
View File
@@ -1,89 +1,98 @@
<template> <template>
<div class="operation-log"> <div class="operation-log">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="操作人"> <div class="filter-section">
<el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="操作类型"> <el-form-item label="操作">
<el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable> <el-input v-model="queryParams.operator" placeholder="请输入操作人" clearable />
<el-option label="登录" value="login" /> </el-form-item>
<el-option label="新增" value="create" /> <el-form-item label="操作类型">
<el-option label="修改" value="update" /> <el-select v-model="queryParams.type" placeholder="请选择操作类型" clearable style="width: 160px">
<el-option label="删除" value="delete" /> <el-option label="登录" value="login" />
</el-select> <el-option label="新增" value="create" />
</el-form-item> <el-option label="修改" value="update" />
<el-form-item label="操作时间"> <el-option label="删除" value="delete" />
<el-date-picker </el-select>
v-model="queryParams.dateRange" </el-form-item>
type="daterange" <el-form-item label="操作时间">
range-separator="至" <el-date-picker
start-placeholder="开始日期" v-model="queryParams.dateRange"
end-placeholder="结束日期" type="daterange"
value-format="YYYY-MM-DD" range-separator="至"
/> start-placeholder="开始日期"
</el-form-item> end-placeholder="结束日期"
<el-form-item> value-format="YYYY-MM-DD"
<el-button type="primary" @click="handleQuery">查询</el-button> style="width: 240px"
<el-button @click="resetQuery">重置</el-button> />
</el-form-item> </el-form-item>
</el-form> <el-form-item>
<div class="action-bar"> <el-button type="primary" @click="handleQuery">
<el-button type="success"> <el-icon><Search /></el-icon>查询
<el-icon><upload /></el-icon>导入 </el-button>
</el-button> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleExport"> </el-form-item>
<el-icon><download /></el-icon>导出 </el-form>
</el-button> <div class="action-bar">
<el-button type="success">
<el-icon><Upload /></el-icon>导入
</el-button>
<el-button type="primary" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 日志列表 --> <!-- 日志列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="logList" :data="logList"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column prop="id" label="ID" width="80" /> >
<el-table-column label="操作人" min-width="150"> <el-table-column prop="id" label="ID" width="80" />
<template #default="{ row }"> <el-table-column label="操作人" min-width="150">
<div class="operator-info"> <template #default="{ row }">
<el-avatar :size="32" :src="row.avatar"></el-avatar> <div class="operator-info">
<div class="operator-detail"> <el-avatar :size="32" :src="row.avatar"></el-avatar>
<div class="username">{{ row.operator }}</div> <div class="operator-detail">
<div class="ip">{{ row.ip }}</div> <div class="username">{{ row.operator }}</div>
<div class="ip">{{ row.ip }}</div>
</div>
</div> </div>
</div> </template>
</template> </el-table-column>
</el-table-column> <el-table-column prop="type" label="操作类型" width="120">
<el-table-column prop="type" label="操作类型" width="120"> <template #default="{ row }">
<template #default="{ row }"> <el-tag :type="getTypeTag(row.type)">{{ getTypeText(row.type) }}</el-tag>
<el-tag :type="getTypeTag(row.type)">{{ getTypeText(row.type) }}</el-tag> </template>
</template> </el-table-column>
</el-table-column> <el-table-column prop="description" label="操作描述" min-width="200" />
<el-table-column prop="description" label="操作描述" min-width="200" /> <el-table-column prop="createTime" label="操作时间" width="180" />
<el-table-column prop="createTime" label="操作时间" width="180" /> <el-table-column label="操作" width="120" fixed="right">
<el-table-column label="操作" width="120" fixed="right"> <template #default="{ row }">
<template #default="{ row }"> <el-button type="primary" link @click="handleDetail(row)">详情</el-button>
<el-button type="primary" link @click="handleDetail(row)">详情</el-button> </template>
</template> </el-table-column>
</el-table-column> </el-table>
</el-table>
<!-- 分页 -->
<!-- 分页 --> <el-pagination
<el-pagination v-model:current-page="queryParams.pageNum"
v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
layout="total, sizes, prev, pager, next, jumper" :total="total"
:total="total" @size-change="handleSizeChange"
@size-change="handleSizeChange" @current-change="handleCurrentChange"
@current-change="handleCurrentChange" background
background class="pagination"
class="pagination" />
/> </div>
</el-card> </el-card>
<!-- 详情对话框 --> <!-- 详情对话框 -->
@@ -117,7 +126,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Upload, Download } from '@element-plus/icons-vue' import { Upload, Download, Search } from '@element-plus/icons-vue'
// //
const queryParams = reactive({ const queryParams = reactive({
@@ -247,57 +256,87 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
.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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
} }
.action-bar { .action-bar {
margin-top: 10px;
display: flex; display: flex;
flex-wrap: wrap;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
} }
/* 表格样式优化 */ /* 表格样式优化 */
:deep(.el-table) { :deep(.el-table) {
border-radius: 8px; border: none;
overflow: hidden; color: #2c3e50;
background: transparent; }
:deep(.el-table__header) {
background: #f8f9fa;
} }
:deep(.el-table th) { :deep(.el-table th) {
background-color: #f8f9fb !important; background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600; font-weight: 600;
color: #1f2937; font-size: 13px;
height: 50px;
padding: 8px 0;
} }
:deep(.el-table td) { :deep(.el-table td) {
padding: 12px 0; border-bottom: 1px solid #f0f2f5;
color: #34495e;
} }
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) { :deep(.el-table tr:hover > td) {
background: #f8fafc; background-color: #f8f9fa !important;
} }
:deep(.el-table__body tr:hover > td) { :deep(.el-card__body) {
background-color: #f1f5f9 !important; padding: 0;
}
:deep(.el-table__body tr) {
transition: all 0.3s ease;
} }
.operator-info { .operator-info {
@@ -322,37 +361,6 @@ onMounted(() => {
color: #64748b; color: #64748b;
} }
/* 分页样式优化 */
.pagination {
margin-top: 24px;
justify-content: flex-end;
padding: 0 16px;
}
:deep(.el-pagination) {
--el-pagination-hover-color: #1f2937;
}
:deep(.el-pagination button:disabled) {
background-color: #f1f5f9;
}
:deep(.el-pagination .el-pager li) {
border-radius: 4px;
margin: 0 2px;
transition: all 0.3s ease;
}
:deep(.el-pagination .el-pager li.active) {
background-color: #1f2937;
color: white;
font-weight: bold;
}
:deep(.el-pagination .el-pager li:hover:not(.active)) {
background-color: #f1f5f9;
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -367,29 +375,4 @@ onMounted(() => {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
} }
</style>
/* 响应式优化 */
@media (max-width: 768px) {
.el-form-item {
margin-bottom: 12px;
}
.action-bar {
flex-direction: column;
align-items: stretch;
}
.action-bar .el-button {
width: 100%;
margin-left: 0 !important;
}
:deep(.el-table th) {
padding: 6px 0;
}
:deep(.el-table td) {
padding: 8px 0;
}
}
</style>
+177 -115
View File
@@ -1,117 +1,120 @@
<template> <template>
<div class="permission-admin-container"> <div class="permission-admin-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="类型"> <div class="filter-section">
<el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 150px" @change="handleOwnerTypeChange"> <div class="filter-content">
<el-option label="用户" value="user" /> <el-form :inline="true" :model="queryParams" class="search-form">
<el-option label="组" value="group" /> <el-form-item label="类型">
</el-select> <el-select v-model="queryParams.owner_type" placeholder="请选择类型" clearable style="width: 150px" @change="handleOwnerTypeChange">
</el-form-item> <el-option label="用户" value="user" />
<el-form-item label="用户" v-if="queryParams.owner_type === 'user'"> <el-option label="" value="group" />
<!-- <el-select v-model="queryParams.user_id" placeholder="请选择用户" clearable filterable style="width: 200px"> </el-select>
<el-option v-for="item in userOptions" :key="item.UserId" :label="`${item.UserName} (ID: ${item.UserId})`" :value="item.UserId" /> </el-form-item>
</el-select> --> <el-form-item label="用户" v-if="queryParams.owner_type === 'user'">
<div class="user_selector-inline">
<div class="user_selector-inline"> <el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;">
<el-tag v-if="queryParams.user_id" type="primary" closable @close="clearQueryUser" style="margin-right: 8px;"> {{ getQueryUserName() }}
{{ getQueryUserName() }} </el-tag>
</el-tag> <el-button type="primary" plain @click="openQueryUserSelector" size="default">
<el-button type="primary" plain @click="openQueryUserSelector" size="default"> <el-icon><User /></el-icon>
<el-icon><User /></el-icon> {{ queryParams.user_id ? '重新选择' : '选择用户' }}
{{ queryParams.user_id ? '重新选择' : '选择用户' }} </el-button>
</div>
</el-form-item>
<el-form-item label="管理员组" v-if="queryParams.owner_type === 'group'">
<el-select v-model="queryParams.admin_group_id" placeholder="请选择管理员组" clearable filterable style="width: 200px">
<el-option v-for="item in adminGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" />
</el-select>
</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-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>分配权限
</el-button>
<el-button type="success" @click="fetchAdminPermissionList">
<el-icon><Refresh/></el-icon>刷新
</el-button> </el-button>
</div> </div>
</el-form-item> </div>
<el-form-item label="管理员组" v-if="queryParams.owner_type === 'group'"> </div>
<el-select v-model="queryParams.admin_group_id" placeholder="请选择管理员组" clearable filterable style="width: 200px">
<el-option v-for="item in adminGroupOptions" :key="item.id" :label="`${item.name} (ID: ${item.id})`" :value="item.id" /> <!-- 管理员权限列表 -->
</el-select> <div class="table-section">
</el-form-item> <el-table
<el-form-item> v-loading="loading"
<el-button type="primary" @click="handleQuery"> :data="adminPermissionList"
<el-icon><Search /></el-icon>查询 style="width: 100%"
</el-button> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-button @click="resetQuery">重置</el-button> >
</el-form-item> <el-table-column prop="id" label="ID" width="80" />
</el-form> <el-table-column label="拥有者类型" width="120">
<div class="action-bar"> <template #default="{ row }">
<el-button type="primary" @click="handleAdd"> <el-tag :type="row.ownerType === 'user' ? 'primary' : 'success'">
<el-icon><Plus /></el-icon>分配权限 {{ row.ownerType === 'user' ? '用户' : '组' }}
</el-button> </el-tag>
<el-button type="success" @click="fetchAdminPermissionList"> </template>
<el-icon><Refresh/></el-icon>刷新 </el-table-column>
</el-button> <el-table-column label="拥有者" width="180">
<template #default="{ row }">
<span v-if="row.ownerType === 'user'">用户ID: {{ row.userId }}</span>
<span v-else>管理员组ID: {{ row.groupId }}</span>
</template>
</el-table-column>
<el-table-column prop="permissionId" label="路径权限ID" width="120" />
<el-table-column label="权限路径" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.permission?.path || '-' }}
</template>
</el-table-column>
<el-table-column label="权限名称" width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.permission?.name || '-' }}
</template>
</el-table-column>
<el-table-column prop="weight" label="权重" width="100" />
<el-table-column label="权限类型" width="120">
<template #default="{ row }">
<el-tag :type="getPermissionTypeTag(row.permissionType)">
{{ getPermissionTypeText(row.permissionType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ formatDate(row.expireAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div> </div>
</el-card> </el-card>
<!-- 管理员权限列表 -->
<el-card class="table-container" shadow="never">
<el-table
v-loading="loading"
:data="adminPermissionList"
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="拥有者类型" width="120">
<template #default="{ row }">
<el-tag :type="row.ownerType === 'user' ? 'primary' : 'success'">
{{ row.ownerType === 'user' ? '用户' : '组' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="拥有者" width="180">
<template #default="{ row }">
<span v-if="row.ownerType === 'user'">用户ID: {{ row.userId }}</span>
<span v-else>管理员组ID: {{ row.groupId }}</span>
</template>
</el-table-column>
<el-table-column prop="permissionId" label="路径权限ID" width="120" />
<el-table-column label="权限路径" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.permission?.path || '-' }}
</template>
</el-table-column>
<el-table-column label="权限名称" width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.permission?.name || '-' }}
</template>
</el-table-column>
<el-table-column prop="weight" label="权重" width="100" />
<el-table-column label="权限类型" width="120">
<template #default="{ row }">
<el-tag :type="getPermissionTypeTag(row.permissionType)">
{{ getPermissionTypeText(row.permissionType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ formatDate(row.expireAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 用户选择弹窗 --> <!-- 用户选择弹窗 -->
<el-dialog <el-dialog
v-model="userSelectorVisible" v-model="userSelectorVisible"
@@ -745,26 +748,55 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -774,5 +806,35 @@ onMounted(() => {
margin-top: 4px; margin-top: 4px;
line-height: 1.5; line-height: 1.5;
} }
</style>
/* 表格样式优化 */
: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>
+124 -63
View File
@@ -1,61 +1,65 @@
<template> <template>
<div class="permission-route-container"> <div class="permission-route-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="关键词"> <div class="filter-section">
<el-input v-model="queryParams.key" placeholder="请输入关键词搜索" clearable style="width: 250px" /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item> <el-form-item label="关键词">
<el-button type="primary" @click="handleQuery"> <el-input v-model="queryParams.key" placeholder="请输入关键词搜索" clearable style="width: 250px" />
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleAdd"> </el-form-item>
<el-icon><Plus /></el-icon>新增路由权限 </el-form>
</el-button> <div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-button type="success" @click="fetchPermissionList"> <el-icon><Plus /></el-icon>新增路由权限
<el-icon><Refresh /></el-icon>刷新 </el-button>
</el-button> <el-button type="success" @click="fetchPermissionList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 路由权限列表 --> <!-- 路由权限列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="permissionList" :data="permissionList"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column prop="id" label="ID" width="80" /> >
<el-table-column prop="name" label="权限名称" min-width="200" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="path" label="路由路径" min-width="300" /> <el-table-column prop="name" label="权限名称" min-width="200" />
<el-table-column prop="note" label="说明" min-width="250" show-overflow-tooltip /> <el-table-column prop="path" label="路由路径" min-width="300" />
<el-table-column label="操作" width="200" fixed="right"> <el-table-column prop="note" label="说明" min-width="250" show-overflow-tooltip />
<template #default="{ row }"> <el-table-column label="操作" width="200" fixed="right">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
</template> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</el-table-column> </template>
</el-table> </el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <!-- 分页 -->
v-model:current-page="queryParams.page" <el-pagination
v-model:page-size="queryParams.count" v-model:current-page="queryParams.page"
:page-sizes="[10, 20, 50, 100]" v-model:page-size="queryParams.count"
layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"
:total="total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" :total="total"
@current-change="handleCurrentChange" @size-change="handleSizeChange"
background @current-change="handleCurrentChange"
class="pagination" background
/> class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 路由权限表单对话框 --> <!-- 路由权限表单对话框 -->
@@ -91,7 +95,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue' import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import { import {
getPermissionList, getPermissionList,
addPermissionInfo, addPermissionInfo,
@@ -255,27 +259,84 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
/* 表格样式优化 */
: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>
+164 -99
View File
@@ -1,98 +1,104 @@
<template> <template>
<div class="setting-container"> <div class="setting-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="配置组"> <div class="filter-section">
<el-select v-model="queryParams.group_id" placeholder="请选择配置组" clearable style="width: 200px" @change="handleQuery"> <div class="filter-content">
<el-option <el-form :inline="true" :model="queryParams" class="search-form">
v-for="group in groupList" <el-form-item label="配置组">
:key="group.id" <el-select v-model="queryParams.group_id" placeholder="请选择配置组" clearable style="width: 200px" @change="handleQuery">
:label="group.name" <el-option
:value="group.id" v-for="group in groupList"
/> :key="group.id"
</el-select> :label="group.name"
</el-form-item> :value="group.id"
<el-form-item label="关键词筛选"> />
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" /> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item label="关键词筛选">
<el-button type="primary" @click="handleQuery"> <el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleAdd"> </el-form-item>
<el-icon><Plus /></el-icon>新增配置 </el-form>
</el-button> <div class="action-bar">
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="primary" @click="handleAdd">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Plus /></el-icon>新增配置
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 配置列表 --> <!-- 配置列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="settingList" :data="settingList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column type="selection" width="55" /> >
<el-table-column prop="id" label="ID" width="80" /> <el-table-column type="selection" width="55" />
<el-table-column prop="name" label="名称" min-width="150" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="value" label="" min-width="200" show-overflow-tooltip> <el-table-column prop="name" label="名称" min-width="150" />
<template #default="{ row }"> <el-table-column prop="value" label="值" min-width="200" show-overflow-tooltip>
<span v-if="row.type === 'bool'">{{ row.value ? '' : '' }}</span> <template #default="{ row }">
<span v-else>{{ row.value }}</span> <span v-if="row.type === 'bool'">{{ row.value ? '' : '' }}</span>
</template> <span v-else>{{ row.value }}</span>
</el-table-column> </template>
<el-table-column prop="type" label="类型" width="100"> </el-table-column>
<template #default="{ row }"> <el-table-column prop="type" label="类型" width="100">
<el-tag :type="getTypeColor(row.type)"> <template #default="{ row }">
{{ row.type || '未知' }} <el-tag :type="getTypeColor(row.type)">
</el-tag> {{ row.type || '未知' }}
</template> </el-tag>
</el-table-column> </template>
<el-table-column prop="settingGroupID" label="配置组" width="150" /> </el-table-column>
<el-table-column label="是否开放" width="100"> <el-table-column prop="settingGroupID" label="配置组" width="150" />
<template #default="{ row }"> <el-table-column label="是否开放" width="100">
<el-switch <template #default="{ row }">
v-model="row.open" <el-switch
@change="handleToggleOpen(row)" v-model="row.open"
:disabled="toggleLoading === row.id" @change="handleToggleOpen(row)"
/> :disabled="toggleLoading === row.id"
</template> />
</el-table-column> </template>
<el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip /> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column prop="note" label="备注" min-width="200" show-overflow-tooltip />
<template #default="{ row }"> <el-table-column label="创建时间" width="180">
{{ formatDate(row.CreatedAt) }} <template #default="{ row }">
</template> {{ formatDate(row.CreatedAt) }}
</el-table-column> </template>
<el-table-column label="操作" width="200" fixed="right"> </el-table-column>
<template #default="{ row }"> <el-table-column label="操作" width="200" fixed="right">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
</template> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</el-table-column> </template>
</el-table> </el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <!-- 分页 -->
v-model:current-page="queryParams.page" <el-pagination
v-model:page-size="queryParams.count" v-model:current-page="queryParams.page"
:page-sizes="[10, 20, 50, 100]" v-model:page-size="queryParams.count"
layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"
:total="total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" :total="total"
@current-change="handleCurrentChange" @size-change="handleSizeChange"
background @current-change="handleCurrentChange"
class="pagination" background
/> class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 配置表单对话框 --> <!-- 配置表单对话框 -->
@@ -509,27 +515,86 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
/* 表格样式优化 */
: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>
+136 -71
View File
@@ -1,70 +1,76 @@
<template> <template>
<div class="setting-group-container"> <div class="setting-group-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="关键词筛选"> <div class="filter-section">
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item> <el-form-item label="关键词筛选">
<el-button type="primary" @click="handleQuery"> <el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleAdd"> </el-form-item>
<el-icon><Plus /></el-icon>新增配置组 </el-form>
</el-button> <div class="action-bar">
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="primary" @click="handleAdd">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Plus /></el-icon>新增配置组
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 配置组列表 --> <!-- 配置组列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="groupList" :data="groupList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column type="selection" width="55" /> >
<el-table-column prop="id" label="ID" width="80" /> <el-table-column type="selection" width="55" />
<el-table-column prop="name" label="名称" min-width="200" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip /> <el-table-column prop="name" label="名称" min-width="200" />
<el-table-column label="创建时间" width="180"> <el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
<template #default="{ row }"> <el-table-column label="创建时间" width="180">
{{ formatDate(row.CreatedAt) }} <template #default="{ row }">
</template> {{ formatDate(row.CreatedAt) }}
</el-table-column> </template>
<el-table-column label="更新时间" width="180"> </el-table-column>
<template #default="{ row }"> <el-table-column label="更新时间" width="180">
{{ formatDate(row.UpdatedAt) }} <template #default="{ row }">
</template> {{ formatDate(row.UpdatedAt) }}
</el-table-column> </template>
<el-table-column label="操作" width="200" fixed="right"> </el-table-column>
<template #default="{ row }"> <el-table-column label="操作" width="200" fixed="right">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
</template> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</el-table-column> </template>
</el-table> </el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <!-- 分页 -->
v-model:current-page="queryParams.page" <el-pagination
v-model:page-size="queryParams.count" v-model:current-page="queryParams.page"
:page-sizes="[10, 20, 50, 100]" v-model:page-size="queryParams.count"
layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"
:total="total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" :total="total"
@current-change="handleCurrentChange" @size-change="handleSizeChange"
background @current-change="handleCurrentChange"
class="pagination" background
/> class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 配置组表单对话框 --> <!-- 配置组表单对话框 -->
@@ -318,27 +324,86 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
/* 表格样式优化 */
: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>
+166 -102
View File
@@ -1,97 +1,102 @@
<template> <template>
<div class="system-file-container"> <div class="system-file-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="关键词筛选"> <div class="filter-section">
<el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="筛选用户"> <el-form-item label="关键词筛选">
<el-input-number v-model="queryParams.user_id" placeholder="请输入用户ID" :controls="false" clearable style="width: 150px" /> <el-input v-model="queryParams.key" placeholder="请输入关键词" clearable style="width: 200px" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item label="筛选用户">
<el-button type="primary" @click="handleQuery"> <el-input-number v-model="queryParams.user_id" placeholder="请输入用户ID" :controls="false" clearable style="width: 150px" />
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery">
</el-form-item> <el-icon><Search /></el-icon>查询
</el-form> </el-button>
<div class="action-bar"> <el-button @click="resetQuery">重置</el-button>
<el-button type="primary" @click="handleUpload"> </el-form-item>
<el-icon><Upload /></el-icon>上传文件 </el-form>
</el-button> <div class="action-bar">
<el-button type="primary" @click="handleUpload">
<el-button type="success" @click="fetchFileList"> <el-icon><Upload /></el-icon>上传文件
<el-icon><Refresh/></el-icon>刷新 </el-button>
</el-button>
<el-button type="success" @click="fetchFileList">
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-icon><Refresh/></el-icon>刷新
<el-icon><Delete /></el-icon>批量删除 </el-button>
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 文件列表 --> <!-- 文件列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="fileList" :data="fileList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column type="selection" width="55" /> >
<el-table-column prop="id" label="ID" width="80" /> <el-table-column type="selection" width="55" />
<el-table-column prop="realName" label="真实文件名" min-width="200" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="saveName" label="保存名称" min-width="150" /> <el-table-column prop="realName" label="真实文件名" min-width="200" />
<el-table-column prop="savePath" label="保存路径" min-width="250" show-overflow-tooltip /> <el-table-column prop="saveName" label="保存名称" min-width="150" />
<el-table-column prop="size" label="文件大小" width="120"> <el-table-column prop="savePath" label="保存路径" min-width="250" show-overflow-tooltip />
<template #default="{ row }"> <el-table-column prop="size" label="文件大小" width="120">
{{ formatFileSize(row.size) }} <template #default="{ row }">
</template> {{ formatFileSize(row.size) }}
</el-table-column> </template>
<el-table-column prop="type" label="文件类型" width="120"> </el-table-column>
<template #default="{ row }"> <el-table-column prop="type" label="文件类型" width="120">
<el-tag :type="getFileTypeColor(row.type)"> <template #default="{ row }">
{{ row.type || '未知' }} <el-tag :type="getFileTypeColor(row.type)">
</el-tag> {{ row.type || '未知' }}
</template> </el-tag>
</el-table-column> </template>
<el-table-column prop="userId" label="用户ID" width="100" /> </el-table-column>
<el-table-column label="是否公开" width="100"> <el-table-column prop="userId" label="用户ID" width="100" />
<template #default="{ row }"> <el-table-column label="是否公开" width="100">
<el-tag :type="row.openDow ? 'success' : 'info'"> <template #default="{ row }">
{{ row.openDow ? '公开' : '私有' }} <el-tag :type="row.openDow ? 'success' : 'info'">
</el-tag> {{ row.openDow ? '公开' : '私有' }}
</template> </el-tag>
</el-table-column> </template>
<el-table-column label="创建时间" width="180"> </el-table-column>
<template #default="{ row }"> <el-table-column label="创建时间" width="180">
{{ formatDate(row.CreatedAt) }} <template #default="{ row }">
</template> {{ formatDate(row.CreatedAt) }}
</el-table-column> </template>
<el-table-column label="操作" width="200" fixed="right"> </el-table-column>
<template #default="{ row }"> <el-table-column label="操作" width="200" fixed="right">
<el-button type="primary" link @click="handleView(row)">查看</el-button> <template #default="{ row }">
<el-button type="success" link @click="handleDownload(row)">下载</el-button> <el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button> <el-button type="success" link @click="handleDownload(row)">下载</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> <el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
</template> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</el-table-column> </template>
</el-table> </el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination <!-- 分页 -->
v-model:current-page="queryParams.page" <el-pagination
v-model:page-size="queryParams.count" v-model:current-page="queryParams.page"
:page-sizes="[10, 20, 50, 100]" v-model:page-size="queryParams.count"
layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]"
:total="total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" :total="total"
@current-change="handleCurrentChange" @size-change="handleSizeChange"
background @current-change="handleCurrentChange"
class="pagination" background
/> class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 文件详情对话框 --> <!-- 文件详情对话框 -->
@@ -250,7 +255,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Upload, Delete, Search, Document, VideoPlay, Folder, UploadFilled, Picture } from '@element-plus/icons-vue' import { Upload, Delete, Search, Document, VideoPlay, Folder, UploadFilled, Picture, Refresh } from '@element-plus/icons-vue'
import { getFileList, getFileDetail, updateFile, deleteFile, uploadFile } from '@/api/admin/file' import { getFileList, getFileDetail, updateFile, deleteFile, uploadFile } from '@/api/admin/file'
// //
@@ -672,22 +677,56 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
} }
.action-bar { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
} }
.file-icon { .file-icon {
@@ -700,11 +739,6 @@ onMounted(() => {
border-radius: 4px; border-radius: 4px;
} }
.pagination {
margin-top: 24px;
justify-content: flex-end;
}
.file-detail-container { .file-detail-container {
padding: 10px 0; padding: 10px 0;
} }
@@ -767,5 +801,35 @@ onMounted(() => {
:deep(.el-descriptions__label) { :deep(.el-descriptions__label) {
width: 120px; width: 120px;
} }
</style>
/* 表格样式优化 */
: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>
+173 -190
View File
@@ -1,103 +1,112 @@
<template> <template>
<div class="users-container"> <div class="users-container">
<!-- 搜索和操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="用户名"> <div class="filter-section">
<el-input v-model="queryParams.username" placeholder="请输入用户名" clearable /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="状态"> <el-form-item label="用户名">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable> <el-input v-model="queryParams.username" placeholder="请输入用户名" clearable />
<el-option label="启用" value="1" /> </el-form-item>
<el-option label="禁用" value="0" /> <el-form-item label="状态">
</el-select> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 150px">
</el-form-item> <el-option label="启用" value="1" />
<el-form-item label="创建时间"> <el-option label="禁用" value="0" />
<el-date-picker </el-select>
v-model="queryParams.dateRange" </el-form-item>
type="daterange" <el-form-item label="创建时间">
range-separator="至" <el-date-picker
start-placeholder="开始日期" v-model="queryParams.dateRange"
end-placeholder="结束日期" type="daterange"
value-format="YYYY-MM-DD" range-separator="至"
/> start-placeholder="开始日期"
</el-form-item> end-placeholder="结束日期"
<el-form-item> value-format="YYYY-MM-DD"
<el-button type="primary" @click="handleQuery">查询</el-button> style="width: 240px"
<el-button @click="resetQuery">重置</el-button> />
</el-form-item> </el-form-item>
</el-form> <el-form-item>
<div class="action-bar"> <el-button type="primary" @click="handleQuery">
<el-button type="primary" @click="handleAdd"> <el-icon><Search /></el-icon>查询
<el-icon><plus /></el-icon>新增用户 </el-button>
</el-button> <el-button @click="resetQuery">重置</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> </el-form-item>
<el-icon><delete /></el-icon>批量删除 </el-form>
</el-button> <div class="action-bar">
<el-button type="success"> <el-button type="primary" @click="handleAdd">
<el-icon><upload /></el-icon>导入 <el-icon><Plus /></el-icon>新增用户
</el-button> </el-button>
<el-button> <el-button type="success">
<el-icon><download /></el-icon> <el-icon><Upload /></el-icon>
</el-button> </el-button>
<el-button type="primary" plain>
<el-icon><Download /></el-icon>导出
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</div>
</div> </div>
</el-card>
<!-- 用户列表 --> <!-- 用户列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <el-table
v-loading="loading" v-loading="loading"
:data="userList" :data="userList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<el-table-column type="selection" width="55" /> >
<el-table-column prop="id" label="ID" width="80" /> <el-table-column type="selection" width="55" />
<el-table-column label="用户信息" min-width="250"> <el-table-column prop="id" label="ID" width="80" />
<template #default="{ row }"> <el-table-column label="用户信息" min-width="250">
<div class="user-info"> <template #default="{ row }">
<el-avatar :size="40" :src="row.avatar"></el-avatar> <div class="user-info">
<div class="user-detail"> <el-avatar :size="40" :src="row.avatar"></el-avatar>
<div class="username">{{ row.username }}</div> <div class="user-detail">
<div class="email">{{ row.email }}</div> <div class="username">{{ row.username }}</div>
<div class="email">{{ row.email }}</div>
</div>
</div> </div>
</div> </template>
</template> </el-table-column>
</el-table-column> <el-table-column prop="role" label="角色" />
<el-table-column prop="role" label="角色" /> <el-table-column prop="phone" label="手机号码" />
<el-table-column prop="phone" label="手机号码" /> <el-table-column label="状态" width="100">
<el-table-column label="状态" width="100"> <template #default="{ row }">
<template #default="{ row }"> <el-switch
<el-switch v-model="row.status"
v-model="row.status" :active-value="1"
:active-value="1" :inactive-value="0"
:inactive-value="0" @change="(val) => handleStatusChange(row, val)"
@change="(val) => handleStatusChange(row, val)" />
/> </template>
</template> </el-table-column>
</el-table-column> <el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="220" fixed="right">
<el-table-column label="操作" width="180" fixed="right"> <template #default="{ row }">
<template #default="{ row }"> <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-button type="primary" link @click="handleRoleAssign(row)">分配角色</el-button>
<el-button type="primary" link @click="handleRoleAssign(row)">分配角色</el-button> <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> </template>
</template> </el-table-column>
</el-table-column> </el-table>
</el-table>
<!-- 分页 -->
<!-- 分页 --> <el-pagination
<el-pagination v-model:current-page="queryParams.pageNum"
v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
layout="total, sizes, prev, pager, next, jumper" :total="total"
:total="total" @size-change="handleSizeChange"
@size-change="handleSizeChange" @current-change="handleCurrentChange"
@current-change="handleCurrentChange" background
background class="pagination"
class="pagination" />
/> </div>
</el-card> </el-card>
<!-- 用户表单对话框 --> <!-- 用户表单对话框 -->
@@ -168,7 +177,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Upload, Download } from '@element-plus/icons-vue' import { Plus, Delete, Upload, Download, Search } from '@element-plus/icons-vue'
// //
const queryParams = reactive({ const queryParams = reactive({
@@ -439,57 +448,56 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
.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 { .search-form {
margin-bottom: 15px; margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
} }
.action-bar { .action-bar {
margin-top: 10px;
display: flex; display: flex;
flex-wrap: wrap;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { .table-section {
border-radius: 8px; padding: 0;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
/* 表格样式优化 */ .pagination {
:deep(.el-table) { margin-top: 20px;
border-radius: 8px; padding: 16px 20px;
overflow: hidden; border-top: 1px solid #e1e8ed;
background: transparent; background: #fafbfc;
} justify-content: flex-end;
:deep(.el-table th) {
background-color: #f8f9fb !important;
font-weight: 600;
color: #1f2937;
height: 50px;
padding: 8px 0;
}
:deep(.el-table td) {
padding: 12px 0;
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: #f8fafc;
}
:deep(.el-table__body tr:hover > td) {
background-color: #f1f5f9 !important;
}
:deep(.el-table__body tr) {
transition: all 0.3s ease;
} }
.user-info { .user-info {
@@ -514,65 +522,40 @@ onMounted(() => {
color: #64748b; color: #64748b;
} }
/* 分页样式优化 */
.pagination {
margin-top: 24px;
justify-content: flex-end;
padding: 0 16px;
}
:deep(.el-pagination) {
--el-pagination-hover-color: #1f2937;
}
:deep(.el-pagination button:disabled) {
background-color: #f1f5f9;
}
:deep(.el-pagination .el-pager li) {
border-radius: 4px;
margin: 0 2px;
transition: all 0.3s ease;
}
:deep(.el-pagination .el-pager li.active) {
background-color: #1f2937;
color: white;
font-weight: bold;
}
:deep(.el-pagination .el-pager li:hover:not(.active)) {
background-color: #f1f5f9;
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 12px; gap: 12px;
} }
/* 响应式优化 */ /* 表格样式优化 */
@media (max-width: 768px) { :deep(.el-table) {
.el-form-item { border: none;
margin-bottom: 12px; color: #2c3e50;
}
.action-bar {
flex-direction: column;
align-items: stretch;
}
.action-bar .el-button {
width: 100%;
margin-left: 0 !important;
}
:deep(.el-table th) {
padding: 6px 0;
}
:deep(.el-table td) {
padding: 8px 0;
}
} }
</style>
: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>
+318 -132
View File
@@ -33,7 +33,7 @@
@click="selectTicket(ticket)" @click="selectTicket(ticket)"
> >
<div class="ticket-avatar"> <div class="ticket-avatar">
<el-avatar :size="40">{{ ticket.username.charAt(0) }}</el-avatar> <el-avatar :size="40" :src="ticket.avatar">{{ ticket.username.charAt(0) }}</el-avatar>
</div> </div>
<div class="ticket-content"> <div class="ticket-content">
<div class="ticket-top"> <div class="ticket-top">
@@ -96,8 +96,8 @@
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']" :class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
> >
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem"> <div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="getUserAvatar(message.userId)"> <el-avatar :size="36" :src="message.avatar">
{{ currentTicket.username.charAt(0) }} {{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
</el-avatar> </el-avatar>
</div> </div>
<div class="message-content"> <div class="message-content">
@@ -117,7 +117,7 @@
<div class="message-time">{{ formatMessageTime(message.time) }}</div> <div class="message-time">{{ formatMessageTime(message.time) }}</div>
</div> </div>
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem"> <div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
<el-avatar :size="36" :src="getUserAvatar(message.userId || 1)">A</el-avatar> <el-avatar :size="36" :src="message.avatar">A</el-avatar>
</div> </div>
</div> </div>
</div> </div>
@@ -204,21 +204,24 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Loading } from '@element-plus/icons-vue' import { Search, Plus, Loading } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { import {
getTickerList, getTickerList,
getTicketDetail, getTicketDetail,
replyTicket, replyTicket,
closeTicket, closeTicket,
getUserAvatar, getUserAvatar,
getFileImage, getFileImage,
parseFilesToImages parseFilesToImages,
getTicketCount
} from '@/api/ticket' } from '@/api/ticket'
import notificationSound from '@/assets/7.wav'
import { useUserStore } from '@/store/userStore'
// //
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
// IDID // store
const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // IDID const userStore = useUserStore()
// //
const adminAvatar = ref('') const adminAvatar = ref('')
@@ -271,6 +274,11 @@ const stats = reactive({
isLoadingStats: false isLoadingStats: false
}) })
//
const previousPendingCount = ref(0)
//
const audio = new Audio(notificationSound)
// //
const quickReplies = ref([ const quickReplies = ref([
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' }, { title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
@@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => {
const mappedTickets = tickets.map(item => ({ const mappedTickets = tickets.map(item => ({
id: item.work_id, id: item.work_id,
title: item.name, title: item.name,
username: `用户${item.user_id}`, // username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user_id, userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(), createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status), status: convertStatusToString(item.status),
@@ -336,9 +345,24 @@ const fetchTicketList = async (append = false) => {
})) }))
if (append) { if (append) {
ticketList.value = [...ticketList.value, ...mappedTickets] // 使work_idwork_id
const existingIds = new Set(ticketList.value.map(t => t.id))
const newTickets = mappedTickets.filter(t => !existingIds.has(t.id))
const mergedTickets = [...ticketList.value, ...newTickets]
// work_id
ticketList.value = mergedTickets.sort((a, b) => {
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
return idB - idA
})
} else { } else {
ticketList.value = mappedTickets // 使work_id
ticketList.value = mappedTickets.sort((a, b) => {
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
return idB - idA
})
} }
hasMore.value = ticketList.value.length < res.data.all_count hasMore.value = ticketList.value.length < res.data.all_count
@@ -353,44 +377,35 @@ const fetchTicketList = async (append = false) => {
} }
} }
//
const fetchStatusStat = async (status) => {
try {
// API
let statusValue = '';
if (status === 'pending') statusValue = '0';
else if (status === 'processing') statusValue = '1';
else if (status === 'replied') statusValue = '2';
else if (status === 'completed') statusValue = '3';
const res = await getTickerList(10, 1, statusValue) //
if (res.code === 200) {
if (status === '') {
stats.total = res.data.all_count
} else {
stats[status] = res.data.all_count
}
} else {
console.error(`获取${status || '全部'}工单统计失败:`, res.message)
}
} catch (error) {
console.error(`获取${status || '全部'}工单统计出错:`, error)
}
}
// //
const fetchAllStats = async () => { const fetchAllStats = async () => {
stats.isLoadingStats = true stats.isLoadingStats = true
try { try {
// const res = await getTicketCount()
await Promise.all([ if (res.code === 200) {
fetchStatusStat(''), // const data = res.data
fetchStatusStat('pending'), //
fetchStatusStat('processing'), // //
fetchStatusStat('replied'), // if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) {
fetchStatusStat('completed') // try {
]) audio.play().catch(e => console.error('播放提示音失败:', e))
} catch (e) {
console.error('播放提示音出错:', e)
}
}
//
previousPendingCount.value = data.wait_count
stats.total = data.all_count
stats.pending = data.wait_count
stats.replied = data.reply_count
stats.completed = data.close_count
// - - -
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
} else {
console.error('获取工单统计失败:', res.message)
}
} catch (error) { } catch (error) {
console.error('获取工单统计数据出错:', error) console.error('获取工单统计数据出错:', error)
} finally { } finally {
@@ -398,6 +413,12 @@ const fetchAllStats = async () => {
} }
} }
//
const fetchCurrentStatusStat = async () => {
await fetchAllStats()
}
// //
const loadMoreTickets = () => { const loadMoreTickets = () => {
if (!hasMore.value || isLoading.value) return if (!hasMore.value || isLoading.value) return
@@ -436,22 +457,22 @@ const filteredTickets = computed(() => {
) )
} }
// // work_id
return result.sort((a, b) => { return result.sort((a, b) => {
// //
if (a.status === 'pending' && b.status !== 'pending') return -1 if (a.status === 'pending' && b.status !== 'pending') return -1
if (a.status !== 'pending' && b.status === 'pending') return 1 if (a.status !== 'pending' && b.status === 'pending') return 1
// // work_id
const timeA = new Date(a.lastReplyTime || a.createTime) const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
const timeB = new Date(b.lastReplyTime || b.createTime) const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
return timeB - timeA return idB - idA
}) })
}) })
// //
const isAdmin = (userId) => { const isAdmin = (userId) => {
return adminUserIds.includes(userId) return userId === userStore.userInfo?.user_id
} }
// //
@@ -513,25 +534,25 @@ const fetchTicketMessages = async (workId) => {
} }
// //
if (detail.Content && detail.Content.length > 0) { if (detail.content && detail.content.length > 0) {
// 使Promise.all //
const messagesPromises = detail.Content.map(async (msg) => { const messages = detail.content.map((msg) => {
const isAdminMsg = isAdmin(msg.UserId) const isAdminMsg = isAdmin(msg.user?.userId)
const images = await parseFilesToImages(msg.Flies) // flies URL
const images = msg.flies ? msg.flies.map(file => file.url) : []
return { return {
id: msg.Id, id: msg.id,
content: msg.Content !== 'empty' ? msg.Content : null, content: msg.content !== 'empty' ? msg.content : null,
images: images, images: images,
time: new Date(msg.CreatedAt).toLocaleString(), time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
isAdmin: isAdminMsg, isAdmin: isAdminMsg,
isSystem: false, isSystem: false,
userId: msg.UserId userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
} }
}) })
//
const messages = await Promise.all(messagesPromises)
currentMessages.value = messages currentMessages.value = messages
} }
} else { } else {
@@ -562,23 +583,29 @@ const sendMessage = async () => {
const fileIds = [] const fileIds = []
try { try {
// "" //
const inputMsg = messageInput.value.trim()
const inputImages = [...selectedImages.value]
//
messageInput.value = ''
selectedImages.value = []
// loading
const tempMsg = { const tempMsg = {
content: messageInput.value.trim() || null, id: Date.now(), // ID
images: selectedImages.value.length > 0 ? [...selectedImages.value] : null, content: inputMsg || null,
images: inputImages.length > 0 ? inputImages : [],
time: new Date().toLocaleString(), time: new Date().toLocaleString(),
isAdmin: true, isAdmin: true,
isLoading: true, isSystem: false,
userId: userStore.userInfo?.user_id,
avatar: userStore.userInfo?.cover_url || '',
isTempMessage: true isTempMessage: true
} }
currentMessages.value.push(tempMsg) currentMessages.value.push(tempMsg)
//
const inputMsg = messageInput.value
messageInput.value = ''
selectedImages.value = []
// //
await nextTick() await nextTick()
scrollToBottom() scrollToBottom()
@@ -606,6 +633,7 @@ const sendMessage = async () => {
// //
messageInput.value = inputMsg messageInput.value = inputMsg
selectedImages.value = inputImages
ElMessage.error(res.message || '发送失败') ElMessage.error(res.message || '发送失败')
} }
@@ -685,7 +713,8 @@ const filterByStatus = (status) => {
activeStatus.value = status activeStatus.value = status
currentPage.value = 1 // currentPage.value = 1 //
hasMore.value = true // hasMore.value = true //
fetchTicketList() // ticketList.value = [] //
fetchTicketList(false) //
} }
// //
@@ -701,42 +730,149 @@ const updateTicketStats = () => {
// //
const formatMessageTime = (timeStr) => { const formatMessageTime = (timeStr) => {
const date = new Date(timeStr) if (!timeStr) return ''
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
try {
const date = new Date(timeStr)
if (isNaN(date.getTime())) return ''
// HH:MM
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
} catch (e) {
console.error('时间格式化失败:', e)
return ''
}
} }
// //
const formatTime = (timeStr) => { const formatTime = (timeStr) => {
const date = new Date(timeStr) if (!timeStr) return ''; //
const now = new Date()
const diff = now - date // 1
let date;
// try {
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) { // ISO
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) date = new Date(timeStr);
// Invalid Date
if (isNaN(date.getTime())) {
//
const cnTimeMatch = timeStr.match(
/(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/
);
if (cnTimeMatch) {
const [, year, month, day, period, hour, minute, second] = cnTimeMatch;
// /1224
let hour24 = parseInt(hour, 10);
if (period === '下午' && hour24 !== 12) {
hour24 += 12;
}
if (period === '上午' && hour24 === 12) {
hour24 = 0; // 120
}
// 0-1
date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10),
hour24,
parseInt(minute, 10),
parseInt(second, 10)
);
} else {
return '无效时间'; // ISO
}
}
} catch (e) {
console.error('时间解析失败:', e);
return '无效时间';
} }
const now = new Date();
const dateTime = date.getTime();
const nowTime = now.getTime();
const diff = nowTime - dateTime;
// 2//
const isToday = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
// if (isToday) {
if (diff < 7 * 24 * 60 * 60 * 1000) { // 24
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] const hour = String(date.getHours()).padStart(2, '0');
return weekdays[date.getDay()] const minute = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
} }
// // 3
return date.toLocaleDateString() const oneWeek = 7 * 24 * 60 * 60 * 1000;
} if (diff < oneWeek) {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekdays[date.getDay()];
}
// 4YYYY/MM/DD
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}/${month}/${day}`;
};
// //
const formatDate = (timeStr) => { const formatDate = (timeStr) => {
const date = new Date(timeStr) console.log("原始时间字符串:", timeStr);
const now = new Date() if (!timeStr) return ''; //
if (date.toDateString() === now.toDateString()) { let date;
return '今天' // 1. ISO
date = new Date(timeStr);
// 2.
if (isNaN(date.getTime())) {
// xxxxxxxx / xx:xx:xx
const cnTimeReg = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*(上午|下午)\s*(\d{1,2}):(\d{1,2}):(\d{1,2})/;
const match = timeStr.match(cnTimeReg);
if (match) {
const [, year, month, day, period, hour, minute, second] = match;
// 1224
let hour24 = parseInt(hour, 10);
if (period === '下午') {
hour24 = hour24 === 12 ? 12 : hour24 + 12; // 12=121-11+12
} else { //
hour24 = hour24 === 12 ? 0 : hour24; // 12=01-11
}
// Date0-1
date = new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
parseInt(day, 10),
hour24,
parseInt(minute, 10),
parseInt(second, 10)
);
} else {
return '无效时间'; //
}
} }
return date.toLocaleDateString() const now = new Date();
} // 3.
const isToday = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (isToday) {
return '今天';
}
// 4.
const formattedDate = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
return formattedDate;
};
// //
watch(currentTicket, (newVal) => { watch(currentTicket, (newVal) => {
@@ -775,8 +911,8 @@ const startAutoRefresh = () => {
// //
refreshTicketList() refreshTicketList()
// //
fetchAllStats() fetchCurrentStatusStat()
}, refreshInterval) }, refreshInterval)
} }
@@ -800,7 +936,8 @@ const refreshTicketList = async () => {
else if (activeStatus.value === 'completed') statusParam = '3' else if (activeStatus.value === 'completed') statusParam = '3'
} }
const res = await getTickerList(pageSize.value, currentPage.value, statusParam) //
const res = await getTickerList(pageSize.value, 1, statusParam)
if (res.code === 200) { if (res.code === 200) {
const tickets = res.data.data || [] const tickets = res.data.data || []
@@ -809,16 +946,36 @@ const refreshTicketList = async () => {
const mappedTickets = tickets.map(item => ({ const mappedTickets = tickets.map(item => ({
id: item.work_id, id: item.work_id,
title: item.name, title: item.name,
username: `用户${item.user_id}`, username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user_id, userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(), createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(), lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status), status: convertStatusToString(item.status),
content: item.name content: item.name
})) }))
// // work_id
ticketList.value = mappedTickets // 使Map
const ticketMap = new Map()
//
ticketList.value.forEach(ticket => {
ticketMap.set(ticket.id, ticket)
})
//
mappedTickets.forEach(ticket => {
ticketMap.set(ticket.id, ticket)
})
// work_id
ticketList.value = Array.from(ticketMap.values()).sort((a, b) => {
const idA = typeof a.id === 'string' ? parseInt(a.id) || 0 : a.id
const idB = typeof b.id === 'string' ? parseInt(b.id) || 0 : b.id
return idB - idA
})
//
//
hasMore.value = ticketList.value.length < res.data.all_count hasMore.value = ticketList.value.length < res.data.all_count
} }
} catch (error) { } catch (error) {
@@ -826,6 +983,39 @@ const refreshTicketList = async () => {
} }
} }
// URL
const normalizeUrl = (url) => {
if (!url) return ''
return url.split('?')[0]
}
// URL
const areMessagesEqual = (messages1, messages2) => {
if (messages1.length !== messages2.length) return false
for (let i = 0; i < messages1.length; i++) {
const msg1 = messages1[i]
const msg2 = messages2[i]
// ID
if (msg1.id !== msg2.id || msg1.content !== msg2.content) return false
//
if ((msg1.images?.length || 0) !== (msg2.images?.length || 0)) return false
// URL
if (msg1.images && msg2.images) {
for (let j = 0; j < msg1.images.length; j++) {
if (normalizeUrl(msg1.images[j]) !== normalizeUrl(msg2.images[j])) {
return false
}
}
}
}
return true
}
// loading // loading
const refreshTicketMessages = async (workId) => { const refreshTicketMessages = async (workId) => {
try { try {
@@ -838,37 +1028,33 @@ const refreshTicketMessages = async (workId) => {
if (currentTicket.value) { if (currentTicket.value) {
// //
if (currentTicket.value.status !== 'pending') { if (currentTicket.value.status !== 'pending') {
currentTicket.value.status = convertStatusToString(detail.Status) currentTicket.value.status = convertStatusToString(detail.status)
} }
} }
// //
if (detail.Content && detail.Content.length > 0) { if (detail.content && detail.content.length > 0) {
// //
const lastMsgId = currentMessages.value.length > 0 ? const newMessages = detail.content.map((msg) => {
currentMessages.value[currentMessages.value.length - 1].id : 0; const isAdminMsg = isAdmin(msg.user?.userId)
const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId); // flies URL
const images = msg.flies ? msg.flies.map(file => file.url) : []
if (hasNewMessage) {
//
const messagesPromises = detail.Content.map(async (msg) => {
const isAdminMsg = isAdmin(msg.UserId)
const images = await parseFilesToImages(msg.Flies)
return {
id: msg.Id,
content: msg.Content !== 'empty' ? msg.Content : null,
images: images,
time: new Date(msg.CreatedAt).toLocaleString(),
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.UserId
}
})
// return {
const messages = await Promise.all(messagesPromises) id: msg.id,
currentMessages.value = messages content: msg.content !== 'empty' ? msg.content : null,
images: images,
time: msg.created_at || msg.updated_at || new Date().toLocaleString(),
isAdmin: isAdminMsg,
isSystem: false,
userId: msg.user?.userId,
avatar: msg.user?.coverUrl || ''
}
})
// URL
if (!areMessagesEqual(currentMessages.value, newMessages)) {
currentMessages.value = newMessages
// //
nextTick(() => { nextTick(() => {
+242 -82
View File
@@ -1,65 +1,93 @@
<template> <template>
<div class="admin-group-container"> <div class="admin-group-container">
<!-- 操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-row :gutter="20"> <!-- 操作栏 -->
<el-col :span="8"> <div class="filter-section">
<el-input <div class="filter-content">
v-model="queryParams.key" <div class="search-form">
placeholder="搜索关键词" <el-input
clearable v-model="queryParams.key"
@clear="fetchGroupList" placeholder="搜索关键词"
@keyup.enter="fetchGroupList" clearable
> @clear="fetchGroupList"
<template #prefix> @keyup.enter="fetchGroupList"
<el-icon><Search /></el-icon> style="width: 200px"
</template> >
</el-input> <template #prefix>
</el-col> <el-icon><Search /></el-icon>
<el-col :span="16"> </template>
<el-button type="primary" @click="handleAdd"> </el-input>
<el-icon><Plus /></el-icon>新增管理员组 </div>
</el-button> <div class="action-bar">
<el-button type="success" @click="fetchGroupList"> <el-button type="primary" @click="handleAdd">
<el-icon><Refresh /></el-icon> <el-icon><Plus /></el-icon>增管理员组
</el-button> </el-button>
</el-col> <el-button type="success" @click="fetchGroupList">
</el-row> <el-icon><Refresh /></el-icon>刷新
</el-card> </el-button>
</div>
</div>
</div>
<!-- 管理员组列表 --> <!-- 管理员组列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="groupList" <div v-for="i in 5" :key="i" class="skeleton-row">
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-name"></div>
<el-table-column prop="id" label="组ID" width="100" /> <div class="skeleton-cell skeleton-note"></div>
<el-table-column prop="name" label="组名称" min-width="200" /> <div class="skeleton-cell skeleton-time"></div>
<el-table-column prop="note" label="备注" min-width="250" /> <div class="skeleton-cell skeleton-action"></div>
<!-- <el-table-column prop="member_count" label="成员数量" width="120" /> --> </div>
<el-table-column prop="CreatedAt" label="创建时间" width="180" /> </div>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }"> <el-table
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> v-else
<el-button type="success" link @click="handleViewMembers(row)">查看成员</el-button> v-loading="loading"
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> :data="groupList"
</template> style="width: 100%"
</el-table-column> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
</el-table> >
<el-table-column prop="id" label="组ID" width="100" />
<!-- 分页 --> <el-table-column prop="name" label="组名称" min-width="200">
<el-pagination <template #default="{ row }">
v-model:current-page="queryParams.page" <span class="group-name">{{ row.name }}</span>
v-model:page-size="queryParams.count" </template>
:page-sizes="[10, 20, 50, 100]" </el-table-column>
layout="total, sizes, prev, pager, next, jumper" <el-table-column prop="note" label="备注" min-width="250" show-overflow-tooltip />
:total="total" <el-table-column prop="CreatedAt" label="创建时间" width="180" />
@size-change="handleSizeChange" <el-table-column label="操作" width="250" fixed="right">
@current-change="handleCurrentChange" <template #default="{ row }">
background <div class="action-buttons">
class="pagination" <el-button type="primary" link @click="handleEdit(row)">
/> <el-icon><Edit /></el-icon>编辑
</el-button>
<el-button type="success" link @click="handleViewMembers(row)">
<el-icon><User /></el-icon>成员
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 管理员组表单对话框 --> <!-- 管理员组表单对话框 -->
@@ -67,6 +95,7 @@
v-model="dialogVisible" v-model="dialogVisible"
:title="dialogType === 'add' ? '新增管理员组' : '编辑管理员组'" :title="dialogType === 'add' ? '新增管理员组' : '编辑管理员组'"
width="600px" width="600px"
append-to-body
> >
<el-form <el-form
ref="groupFormRef" ref="groupFormRef"
@@ -82,8 +111,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
@@ -92,26 +123,36 @@
v-model="memberDialogVisible" v-model="memberDialogVisible"
title="管理员组成员" title="管理员组成员"
width="900px" width="900px"
append-to-body
> >
<el-input <div class="member-search">
v-model="memberParams.key" <el-input
placeholder="搜索成员" v-model="memberParams.key"
clearable placeholder="搜索成员"
@clear="fetchMemberList" clearable
@keyup.enter="fetchMemberList" @clear="fetchMemberList"
style="margin-bottom: 16px" @keyup.enter="fetchMemberList"
> style="width: 200px"
<template #prefix> >
<el-icon><Search /></el-icon> <template #prefix>
</template> <el-icon><Search /></el-icon>
</el-input> </template>
</el-input>
</div>
<el-table <el-table
v-loading="memberLoading" v-loading="memberLoading"
:data="memberList" :data="memberList"
style="width: 100%" style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column prop="UserId" label="用户ID" width="100" /> <el-table-column prop="UserId" label="用户ID" width="100" />
<el-table-column prop="UserName" label="用户名" min-width="150" /> <el-table-column prop="UserName" label="用户名" min-width="150">
<template #default="{ row }">
<div class="user-info">
<span class="username">{{ row.UserName }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="Email" label="邮箱" min-width="200" /> <el-table-column prop="Email" label="邮箱" min-width="200" />
<el-table-column prop="Phone" label="手机号" width="130" /> <el-table-column prop="Phone" label="手机号" width="130" />
<el-table-column label="性别" width="80"> <el-table-column label="性别" width="80">
@@ -146,7 +187,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Search } from '@element-plus/icons-vue' import { Plus, Refresh, Search, Edit, User, Delete } from '@element-plus/icons-vue'
import { import {
getAdminGroupList, getAdminGroupList,
getAdminGroupMemberList, getAdminGroupMemberList,
@@ -341,18 +382,137 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
} }
.table-container { .filter-section {
border-radius: 8px; 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: 0;
flex: 1;
display: flex;
align-items: center;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.group-name {
font-weight: 500;
color: #2c3e50;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
.member-search {
margin-bottom: 16px;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-id { width: 100px; }
.skeleton-name { width: 200px; }
.skeleton-note { flex: 1; min-width: 250px; }
.skeleton-time { width: 180px; }
.skeleton-action { width: 250px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+43 -2
View File
@@ -1062,7 +1062,8 @@ onBeforeUnmount(() => {
.filter-container { .filter-container {
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 8px; border: 1px solid #e1e8ed;
background: #fafbfc;
} }
.balance-info-card { .balance-info-card {
@@ -1071,7 +1072,8 @@ onBeforeUnmount(() => {
} }
.table-container { .table-container {
border-radius: 8px; border: 1px solid #e1e8ed;
background: #ffffff;
} }
.card-header { .card-header {
@@ -1449,6 +1451,45 @@ onBeforeUnmount(() => {
flex-direction: column; flex-direction: column;
} }
} }
/* 表格样式优化 */
: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: 20px;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
</style> </style>
<style> <style>
File diff suppressed because it is too large Load Diff
+254 -186
View File
@@ -1,87 +1,120 @@
<template> <template>
<div class="user-group-container"> <div class="user-group-container">
<!-- 操作栏 --> <!-- 主容器 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-button type="primary" @click="handleAdd"> <!-- 操作栏 -->
<el-icon><Plus /></el-icon>新增用户组 <div class="filter-section">
</el-button> <div class="filter-content">
<el-button type="success" @click="fetchGroupList"> <div class="action-bar">
<el-icon><Refresh /></el-icon>刷新 <el-button type="primary" @click="handleAdd">
</el-button> <el-icon><Plus /></el-icon>新增用户组
</el-card> </el-button>
<el-button type="success" @click="fetchGroupList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 用户组列表 --> <!-- 用户组列表 -->
<el-card class="table-container" shadow="never"> <div class="table-section">
<el-table <!-- 骨架屏 -->
v-loading="loading" <div v-if="loading" class="skeleton-container">
:data="groupList" <div v-for="i in 5" :key="i" class="skeleton-row">
style="width: 100%" <div class="skeleton-cell skeleton-id"></div>
> <div class="skeleton-cell skeleton-name"></div>
<el-table-column label="组ID" width="100"> <div class="skeleton-cell skeleton-auth"></div>
<template #default="{ row }"> <div class="skeleton-cell skeleton-price"></div>
{{ row.group_id || row.GroupId || row.id || row.Id }} <div class="skeleton-cell skeleton-level"></div>
</template> <div class="skeleton-cell skeleton-type"></div>
</el-table-column> <div class="skeleton-cell skeleton-count"></div>
<el-table-column label="组名称" min-width="200"> <div class="skeleton-cell skeleton-time"></div>
<template #default="{ row }"> <div class="skeleton-cell skeleton-action"></div>
{{ row.group_name || row.name || row.Name }} </div>
</template> </div>
</el-table-column>
<el-table-column label="权限" min-width="200" show-overflow-tooltip> <el-table
<template #default="{ row }"> v-else
<el-tag type="info">{{ row.auth || row.Auth || '-' }}</el-tag> v-loading="loading"
</template> :data="groupList"
</el-table-column> style="width: 100%"
<el-table-column label="升级金额" width="120"> :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
<template #default="{ row }"> >
<span v-if="row.floor_price || row.FloorPrice">¥{{ row.floor_price || row.FloorPrice }}</span> <el-table-column label="组ID" width="100">
<span v-else>-</span> <template #default="{ row }">
</template> {{ row.group_id || row.GroupId || row.id || row.Id }}
</el-table-column> </template>
<el-table-column label="下一级组ID" width="120"> </el-table-column>
<template #default="{ row }"> <el-table-column label="组名称" min-width="200">
{{ row.higher_level_id || row.HigherLevelId || '-' }} <template #default="{ row }">
</template> <span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
</el-table-column> </template>
<el-table-column label="类型" width="100" align="center"> </el-table-column>
<template #default="{ row }"> <el-table-column label="权限" min-width="200" show-overflow-tooltip>
<el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small"> <template #default="{ row }">
{{ (row.fixed || row.Fixed) ? '固定' : '可升级' }} <el-tag type="info" effect="plain">{{ row.auth || row.Auth || '-' }}</el-tag>
</el-tag> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="升级金额" width="120">
<el-table-column label="成员数量" width="100" align="center"> <template #default="{ row }">
<template #default="{ row }"> <span v-if="row.floor_price || row.FloorPrice" class="price-text">¥{{ row.floor_price || row.FloorPrice }}</span>
{{ row.member_count || row.MemberCount || 0 }} <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="160" show-overflow-tooltip> <el-table-column label="下一级组ID" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }} {{ row.higher_level_id || row.HigherLevelId || '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="280" fixed="right"> <el-table-column label="类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button> <el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small">
<el-button type="success" link @click="handleViewMembers(row)">查看成员</el-button> {{ (row.fixed || row.Fixed) ? '固定' : '可升级' }}
<!-- <el-button type="warning" link @click="handleAddMember(row)">添加成员</el-button> --> </el-tag>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button> </template>
</template> </el-table-column>
</el-table-column> <el-table-column label="成员数量" width="100" align="center">
</el-table> <template #default="{ row }">
<el-tag type="info" size="small" effect="plain">
<!-- 分页 --> {{ row.member_count || row.MemberCount || 0 }}
<el-pagination </el-tag>
v-model:current-page="queryParams.page" </template>
v-model:page-size="queryParams.count" </el-table-column>
:page-sizes="[10, 20, 50, 100]" <el-table-column label="创建时间" width="180">
layout="total, sizes, prev, pager, next, jumper" <template #default="{ row }">
:total="total" {{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
@size-change="handleSizeChange" </template>
@current-change="handleCurrentChange" </el-table-column>
background <el-table-column label="操作" width="280" fixed="right">
class="pagination" <template #default="{ row }">
/> <div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button type="success" link @click="handleViewMembers(row)">
<el-icon><User /></el-icon>成员
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card> </el-card>
<!-- 用户组表单对话框 --> <!-- 用户组表单对话框 -->
@@ -90,6 +123,7 @@
:title="dialogType === 'add' ? '新增用户组' : '编辑用户组'" :title="dialogType === 'add' ? '新增用户组' : '编辑用户组'"
width="650px" width="650px"
destroy-on-close destroy-on-close
append-to-body
> >
<el-form <el-form
ref="groupFormRef" ref="groupFormRef"
@@ -137,8 +171,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitForm">确定</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
@@ -147,11 +183,13 @@
v-model="memberDialogVisible" v-model="memberDialogVisible"
title="用户组成员" title="用户组成员"
width="800px" width="800px"
append-to-body
> >
<el-table <el-table
v-loading="memberLoading" v-loading="memberLoading"
:data="memberList" :data="memberList"
style="width: 100%" style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column label="用户ID" width="100"> <el-table-column label="用户ID" width="100">
<template #default="{ row }"> <template #default="{ row }">
@@ -160,7 +198,9 @@
</el-table-column> </el-table-column>
<el-table-column label="用户名" min-width="150"> <el-table-column label="用户名" min-width="150">
<template #default="{ row }"> <template #default="{ row }">
{{ row.username || row.Username || row.UserName || row.name || row.Name }} <div class="user-info">
<span class="username">{{ row.username || row.Username || row.UserName || row.name || row.Name }}</span>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="邮箱" min-width="200"> <el-table-column label="邮箱" min-width="200">
@@ -193,6 +233,7 @@
v-model="addMemberDialogVisible" v-model="addMemberDialogVisible"
title="添加用户组成员" title="添加用户组成员"
width="500px" width="500px"
append-to-body
> >
<el-form <el-form
ref="memberFormRef" ref="memberFormRef"
@@ -205,8 +246,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="addMemberDialogVisible = false">取消</el-button> <div class="dialog-footer">
<el-button type="primary" @click="submitAddMember">确定</el-button> <el-button @click="addMemberDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAddMember">确定</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -215,7 +258,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh } from '@element-plus/icons-vue' import { Plus, Refresh, Edit, User, Delete } from '@element-plus/icons-vue'
import { import {
getUserGroupList, getUserGroupList,
getUserGroupMemberList, getUserGroupMemberList,
@@ -286,16 +329,9 @@ const memberFormRef = ref(null)
// //
const fetchGroupList = async () => { const fetchGroupList = async () => {
console.log('=== 获取用户组列表 ===')
console.log('请求参数:', queryParams)
loading.value = true loading.value = true
try { try {
const res = await getUserGroupList(queryParams) const res = await getUserGroupList(queryParams)
console.log('用户组列表响应:', res)
console.log('响应状态码:', res.data?.code || res.code)
console.log('响应数据:', res.data?.data || res.data)
const code = res.data?.code || res.code const code = res.data?.code || res.code
if (code === 200) { if (code === 200) {
let responseData = res.data?.data || res.data let responseData = res.data?.data || res.data
@@ -303,7 +339,6 @@ const fetchGroupList = async () => {
item.CreatedAt = formatTime(item.CreatedAt) item.CreatedAt = formatTime(item.CreatedAt)
}) })
//
if (Array.isArray(responseData)) { if (Array.isArray(responseData)) {
groupList.value = responseData groupList.value = responseData
total.value = responseData.length total.value = responseData.length
@@ -317,20 +352,10 @@ const fetchGroupList = async () => {
groupList.value = [] groupList.value = []
total.value = 0 total.value = 0
} }
console.log('用户组列表数据:', groupList.value)
console.log('总数:', total.value)
if (groupList.value.length > 0) {
console.log('第一个用户组示例:', groupList.value[0])
}
} else { } else {
console.error('获取用户组列表失败:', res.data)
ElMessage.error(res.data?.message || '获取用户组列表失败') ElMessage.error(res.data?.message || '获取用户组列表失败')
} }
} catch (error) { } catch (error) {
console.error('获取用户组列表错误:', error)
console.error('错误详情:', error.response || error.message)
ElMessage.error('获取用户组列表失败') ElMessage.error('获取用户组列表失败')
} finally { } finally {
loading.value = false loading.value = false
@@ -339,21 +364,12 @@ const fetchGroupList = async () => {
// //
const fetchMemberList = async () => { const fetchMemberList = async () => {
console.log('=== 获取用户组成员列表 ===')
console.log('请求参数:', memberParams)
memberLoading.value = true memberLoading.value = true
try { try {
const res = await getUserGroupMemberList(memberParams) const res = await getUserGroupMemberList(memberParams)
console.log('成员列表响应:', res)
console.log('响应状态码:', res.data?.code || res.code)
console.log('响应数据:', res.data?.data || res.data)
const code = res.data?.code || res.code const code = res.data?.code || res.code
if (code === 200) { if (code === 200) {
const responseData = res.data?.data || res.data const responseData = res.data?.data || res.data
//
if (Array.isArray(responseData)) { if (Array.isArray(responseData)) {
memberList.value = responseData memberList.value = responseData
memberTotal.value = responseData.length memberTotal.value = responseData.length
@@ -367,20 +383,10 @@ const fetchMemberList = async () => {
memberList.value = [] memberList.value = []
memberTotal.value = 0 memberTotal.value = 0
} }
console.log('成员列表数据:', memberList.value)
console.log('成员总数:', memberTotal.value)
if (memberList.value.length > 0) {
console.log('第一个成员示例:', memberList.value[0])
}
} else { } else {
console.error('获取成员列表失败:', res.data)
ElMessage.error(res.data?.message || '获取成员列表失败') ElMessage.error(res.data?.message || '获取成员列表失败')
} }
} catch (error) { } catch (error) {
console.error('获取成员列表错误:', error)
console.error('错误详情:', error.response || error.message)
ElMessage.error('获取成员列表失败') ElMessage.error('获取成员列表失败')
} finally { } finally {
memberLoading.value = false memberLoading.value = false
@@ -410,7 +416,6 @@ const handleMemberCurrentChange = (page) => {
// //
const handleAdd = () => { const handleAdd = () => {
console.log('=== 打开新增用户组对话框 ===')
dialogType.value = 'add' dialogType.value = 'add'
dialogVisible.value = true dialogVisible.value = true
Object.assign(groupForm, { Object.assign(groupForm, {
@@ -426,13 +431,9 @@ const handleAdd = () => {
// //
const handleEdit = (row) => { const handleEdit = (row) => {
console.log('=== 打开编辑用户组对话框 ===')
console.log('用户组数据:', row)
dialogType.value = 'edit' dialogType.value = 'edit'
dialogVisible.value = true dialogVisible.value = true
//
const groupId = row.group_id || row.GroupId || row.id || row.Id const groupId = row.group_id || row.GroupId || row.id || row.Id
const groupName = row.group_name || row.name || row.Name const groupName = row.group_name || row.name || row.Name
const groupAuth = row.auth || row.Auth || '' const groupAuth = row.auth || row.Auth || ''
@@ -448,19 +449,11 @@ const handleEdit = (row) => {
floor_price: floorPrice, floor_price: floorPrice,
fixed: fixed fixed: fixed
}) })
console.log('表单数据:', groupForm)
} }
// //
const handleViewMembers = (row) => { const handleViewMembers = (row) => {
//
const groupId = row.group_id || row.GroupId || row.id || row.Id const groupId = row.group_id || row.GroupId || row.id || row.Id
console.log('=== 查看用户组成员 ===')
console.log('用户组数据:', row)
console.log('组ID:', groupId)
memberParams.group_id = groupId memberParams.group_id = groupId
memberParams.page = 1 memberParams.page = 1
memberDialogVisible.value = true memberDialogVisible.value = true
@@ -469,13 +462,7 @@ const handleViewMembers = (row) => {
// //
const handleAddMember = (row) => { const handleAddMember = (row) => {
//
const groupId = row.group_id || row.GroupId || row.id || row.Id const groupId = row.group_id || row.GroupId || row.id || row.Id
console.log('=== 打开添加成员对话框 ===')
console.log('用户组数据:', row)
console.log('组ID:', groupId)
memberForm.group_id = groupId memberForm.group_id = groupId
memberForm.user_ids = '' memberForm.user_ids = ''
addMemberDialogVisible.value = true addMemberDialogVisible.value = true
@@ -483,15 +470,9 @@ const handleAddMember = (row) => {
// //
const handleDelete = (row) => { const handleDelete = (row) => {
//
const groupId = row.group_id || row.GroupId || row.id || row.Id const groupId = row.group_id || row.GroupId || row.id || row.Id
const groupName = row.group_name || row.name || row.Name const groupName = row.group_name || row.name || row.Name
console.log('=== 删除用户组 ===')
console.log('用户组数据:', row)
console.log('组ID:', groupId)
console.log('组名称:', groupName)
ElMessageBox.confirm(`确认删除用户组 ${groupName} 吗?`, '警告', { ElMessageBox.confirm(`确认删除用户组 ${groupName} 吗?`, '警告', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -499,20 +480,14 @@ const handleDelete = (row) => {
}).then(async () => { }).then(async () => {
try { try {
const res = await deleteUserGroup({ group_id: groupId }) const res = await deleteUserGroup({ group_id: groupId })
console.log('删除响应:', res)
console.log('响应状态码:', res.data?.code || res.code)
const code = res.data?.code || res.code const code = res.data?.code || res.code
if (code === 200) { if (code === 200) {
ElMessage.success('删除成功') ElMessage.success('删除成功')
fetchGroupList() fetchGroupList()
} else { } else {
console.error('删除失败:', res.data)
ElMessage.error(res.data?.message || '删除失败') ElMessage.error(res.data?.message || '删除失败')
} }
} catch (error) { } catch (error) {
console.error('删除错误:', error)
console.error('错误详情:', error.response || error.message)
ElMessage.error('删除失败') ElMessage.error('删除失败')
} }
}).catch(() => {}) }).catch(() => {})
@@ -522,10 +497,6 @@ const handleDelete = (row) => {
const submitForm = () => { const submitForm = () => {
groupFormRef.value?.validate(async (valid) => { groupFormRef.value?.validate(async (valid) => {
if (valid) { if (valid) {
console.log('=== 提交用户组表单 ===')
console.log('操作类型:', dialogType.value)
console.log('表单数据:', groupForm)
try { try {
let res let res
const submitData = { const submitData = {
@@ -533,40 +504,30 @@ const submitForm = () => {
auth: groupForm.auth auth: groupForm.auth
} }
//
if (groupForm.higher_level_id !== undefined && groupForm.higher_level_id !== null) { if (groupForm.higher_level_id !== undefined && groupForm.higher_level_id !== null) {
submitData.higher_level_id = groupForm.higher_level_id submitData.higher_level_id = groupForm.higher_level_id
} }
if (groupForm.floor_price !== undefined && groupForm.floor_price !== null) { if (groupForm.floor_price !== undefined && groupForm.floor_price !== null) {
submitData.floor_price = groupForm.floor_price submitData.floor_price = groupForm.floor_price
} }
// fixed boolean
submitData.fixed = groupForm.fixed submitData.fixed = groupForm.fixed
if (dialogType.value === 'add') { if (dialogType.value === 'add') {
console.log('新增用户组,提交数据:', submitData)
res = await createUserGroup(submitData) res = await createUserGroup(submitData)
} else { } else {
submitData.group_id = groupForm.group_id submitData.group_id = groupForm.group_id
console.log('更新用户组,提交数据:', submitData)
res = await updateUserGroupInfo(submitData) res = await updateUserGroupInfo(submitData)
} }
console.log('提交响应:', res)
console.log('响应状态码:', res.data?.code || res.code)
const code = res.data?.code || res.code const code = res.data?.code || res.code
if (code === 200) { if (code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功') ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false dialogVisible.value = false
fetchGroupList() fetchGroupList()
} else { } else {
console.error('操作失败:', res.data)
ElMessage.error(res.data?.message || '操作失败') ElMessage.error(res.data?.message || '操作失败')
} }
} catch (error) { } catch (error) {
console.error('提交失败:', error)
console.error('错误详情:', error.response || error.message)
ElMessage.error('操作失败') ElMessage.error('操作失败')
} }
} }
@@ -577,26 +538,17 @@ const submitForm = () => {
const submitAddMember = () => { const submitAddMember = () => {
memberFormRef.value?.validate(async (valid) => { memberFormRef.value?.validate(async (valid) => {
if (valid) { if (valid) {
console.log('=== 提交添加成员 ===')
console.log('表单数据:', memberForm)
try { try {
const res = await addUserGroupMember(memberForm) const res = await addUserGroupMember(memberForm)
console.log('添加成员响应:', res)
console.log('响应状态码:', res.data?.code || res.code)
const code = res.data?.code || res.code const code = res.data?.code || res.code
if (code === 200) { if (code === 200) {
ElMessage.success('添加成功') ElMessage.success('添加成功')
addMemberDialogVisible.value = false addMemberDialogVisible.value = false
fetchGroupList() fetchGroupList()
} else { } else {
console.error('添加失败:', res.data)
ElMessage.error(res.data?.message || '添加失败') ElMessage.error(res.data?.message || '添加失败')
} }
} catch (error) { } catch (error) {
console.error('添加错误:', error)
console.error('错误详情:', error.response || error.message)
ElMessage.error('添加失败') ElMessage.error('添加失败')
} }
} }
@@ -605,7 +557,6 @@ const submitAddMember = () => {
// //
onMounted(() => { onMounted(() => {
console.log('=== 用户组管理页面初始化 ===')
fetchGroupList() fetchGroupList()
}) })
</script> </script>
@@ -615,18 +566,135 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; background: #ffffff;
} }
.table-container { .filter-section {
border-radius: 8px; padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.group-name {
font-weight: 500;
color: #2c3e50;
}
.price-text {
color: #f56c6c;
font-weight: 500;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
</style>
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-id { width: 100px; }
.skeleton-name { width: 200px; }
.skeleton-auth { flex: 1; min-width: 200px; }
.skeleton-price { width: 120px; }
.skeleton-level { width: 120px; }
.skeleton-type { width: 100px; }
.skeleton-count { width: 100px; }
.skeleton-time { width: 180px; }
.skeleton-action { width: 280px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
+284 -45
View File
@@ -1,38 +1,68 @@
<template> <template>
<div class="user-list-container"> <div class="user-list-container">
<!-- 搜索和操作栏 --> <!-- 搜索和用户列表 -->
<el-card class="filter-container" shadow="never"> <el-card class="main-container" shadow="never">
<el-form :inline="true" :model="queryParams" class="search-form"> <!-- 搜索和操作栏 -->
<el-form-item label="关键字"> <div class="filter-section">
<el-input v-model="queryParams.key" placeholder="请输入用户名/邮箱" clearable style="width: 200px" /> <div class="filter-content">
</el-form-item> <el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item> <el-form-item label="关键字">
<el-button type="primary" @click="handleQuery"> <el-input v-model="queryParams.key" placeholder="请输入用户名/邮箱" clearable style="width: 200px" />
<el-icon><Search /></el-icon>查询 </el-form-item>
</el-button> <el-form-item>
<el-button @click="resetQuery">重置</el-button> <el-button type="primary" @click="handleQuery">
<el-button type="success" @click="fetchUserList"> <el-icon><Search /></el-icon>查询
<el-icon><Refresh /></el-icon>刷新 </el-button>
</el-button> <el-button @click="resetQuery">重置</el-button>
</el-form-item> <el-button type="success" @click="fetchUserList">
</el-form> <el-icon><Refresh /></el-icon>刷新
<div class="action-bar"> </el-button>
<el-button type="primary" @click="handleAdd"> </el-form-item>
<el-icon><Plus /></el-icon>新增用户 </el-form>
</el-button> <div class="action-bar">
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete"> <el-button type="primary" @click="handleAdd">
<el-icon><Delete /></el-icon>批量删除 <el-icon><Plus /></el-icon>新增用户
</el-button> </el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</el-button>
</div>
</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 <el-table
v-loading="loading" v-else
:data="userList" :data="userList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%" style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="UserId" label="用户ID" width="100" /> <el-table-column prop="UserId" label="用户ID" width="100" />
@@ -123,6 +153,7 @@
background background
class="pagination" class="pagination"
/> />
</div>
</el-card> </el-card>
<!-- 用户编辑对话框 --> <!-- 用户编辑对话框 -->
@@ -894,22 +925,198 @@ onMounted(() => {
padding: 0; padding: 0;
} }
.filter-container { .main-container {
margin-bottom: 20px; border: 1px solid #e1e8ed;
border-radius: 8px; 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 { .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 { .action-bar {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-shrink: 0;
} }
.table-container { @media (max-width: 768px) {
border-radius: 8px; .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 { .user-info {
@@ -926,31 +1133,36 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
margin-bottom: 4px; margin-bottom: 4px;
color: #2c3e50;
} }
.email { .email {
font-size: 12px; font-size: 12px;
color: #999; color: #7f8c8d;
} }
.real-name { .real-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
margin-bottom: 2px; margin-bottom: 2px;
color: #2c3e50;
} }
.id-card { .id-card {
font-size: 12px; font-size: 12px;
color: #666; color: #7f8c8d;
} }
.text-gray { .text-gray {
color: #999; color: #95a5a6;
font-size: 12px; font-size: 12px;
} }
.pagination { .pagination {
margin-top: 24px; margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -958,12 +1170,9 @@ onMounted(() => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 12px; gap: 12px;
padding: 0;
} }
.text-gray {
color: #999;
font-size: 12px;
}
.action-buttons { .action-buttons {
display: flex; display: flex;
@@ -980,13 +1189,12 @@ onMounted(() => {
.avatar-preview { .avatar-preview {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
border-radius: 8px;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: opacity 0.2s ease;
} }
.avatar-preview:hover { .avatar-preview:hover {
transform: scale(1.05); opacity: 0.8;
} }
.avatar-preview:hover .avatar-overlay { .avatar-preview:hover .avatar-overlay {
@@ -1006,7 +1214,7 @@ onMounted(() => {
justify-content: center; justify-content: center;
gap: 4px; gap: 4px;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.2s ease;
color: white; color: white;
} }
@@ -1026,5 +1234,36 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
color: #606266; 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> </style>