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

- Created ImageForm.vue as standalone page for add/edit image functionality
- Removed dialog-based image form from VmImages.vue
- Implemented tagsViewStore for global tab state management
- Added automatic tab closing on form cancel/back
- Fixed data persistence issue when switching between image edits
- Removed quick actions section from ImageForm
- Updated router configuration for new image form route
This commit is contained in:
2025-11-28 14:15:29 +08:00
parent 067e0539ba
commit f7c3be1d30
45 changed files with 8776 additions and 6881 deletions
+166 -179
View File
@@ -1,17 +1,5 @@
<template>
<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="stat-card total-card">
@@ -37,79 +25,91 @@
</div>
</div>
<!-- 搜索和筛选 -->
<div class="filter-section">
<el-input
v-model="filterForm.url"
placeholder="搜索 Guacamole URL"
prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
class="search-input"
/>
<div class="filter-actions">
<el-button type="primary" @click="handleSearch" :icon="Search">搜索</el-button>
<el-button @click="resetFilter" :icon="Delete">重置</el-button>
</div>
</div>
<!-- Guacamole 配置列表 -->
<div class="table-container">
<el-table
v-loading="loading"
:data="guacamoleData"
border
stripe
style="width: 100%"
table-layout="auto"
class="guacamole-table"
>
<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-card class="main-container" shadow="never">
<!-- 搜索和筛选 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="filterForm" class="search-form">
<el-form-item>
<el-input
v-model="filterForm.url"
placeholder="搜索 Guacamole URL"
prefix-icon="Search"
clearable
@keyup.enter="handleSearch"
style="width: 300px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
<el-button @click="resetFilter">
<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>
</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">
<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>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container">
<!-- Guacamole 配置列表 -->
<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
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -119,9 +119,10 @@
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</div>
</el-card>
<!-- 添加/编辑配置对话框 -->
<el-dialog
@@ -510,42 +511,7 @@ onMounted(async () => {
<style scoped>
.guacamole-container {
padding: 20px;
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;
padding: 0;
}
/* 统计卡片 */
@@ -558,18 +524,18 @@ onMounted(async () => {
.stat-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 20px;
display: flex;
align-items: center;
transition: all 0.3s;
border: 1px solid #ebeef5;
border: 1px solid #e1e8ed;
}
.stat-card:hover {
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 {
@@ -608,60 +574,64 @@ onMounted(async () => {
font-weight: 600;
margin-bottom: 4px;
line-height: 1.1;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #606266;
color: #909399;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
/* 搜索和筛选部分 */
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
gap: 16px;
margin-bottom: 24px;
justify-content: space-between;
align-items: center;
background: white;
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-input {
.search-form {
margin: 0;
flex: 1;
max-width: 400px;
}
.filter-actions {
display: flex;
gap: 8px;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
/* 表格容器 */
.table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 16px;
margin-bottom: 24px;
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.guacamole-table {
margin-bottom: 16px;
}
/* 操作按钮 */
.action-buttons {
.action-bar {
display: flex;
justify-content: center;
gap: 8px;
gap: 12px;
flex-shrink: 0;
}
/* 分页 */
.pagination-container {
display: flex;
justify-content: flex-end;
.table-section {
padding: 0;
}
.pagination {
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;
}
.form-section-title {
font-weight: 600;
margin: 16px 0 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
color: #409EFF;
}
/* 对话框底部 */
.dialog-footer {
display: flex;
@@ -696,6 +658,37 @@ onMounted(async () => {
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) {
.stats-panel {
@@ -708,17 +701,6 @@ onMounted(async () => {
}
@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 {
grid-template-columns: 1fr;
}
@@ -727,13 +709,18 @@ onMounted(async () => {
grid-column: auto;
}
.filter-section {
.filter-content {
flex-direction: column;
align-items: stretch;
}
.search-input {
max-width: none;
.search-form {
width: 100%;
}
.action-bar {
width: 100%;
justify-content: flex-start;
}
}
</style>
+118 -172
View File
@@ -1,61 +1,59 @@
<template>
<div class="container-images-container" v-loading="loading">
<div class="page-header">
<h2>容器镜像</h2>
<div class="header-actions">
<el-button type="primary" @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</div>
</div>
<!-- 服务器选择和搜索区域 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="服务器">
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
<el-option
v-for="server in serverList"
:key="server.server_id"
:label="server.name"
:value="server.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" 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-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>
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="服务器">
<el-select v-model="selectedServerId" placeholder="请选择服务器" @change="handleServerChange" style="width: 200px">
<el-option
v-for="server in serverList"
:key="server.server_id"
:label="server.name"
:value="server.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" placeholder="请输入镜像名称" clearable style="width: 200px" />
</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="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
</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
:data="currentMirrorList"
border
style="width: 100%"
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 label="镜像信息" min-width="250">
@@ -65,7 +63,7 @@
<div class="image-info-content">
<div class="image-name-row">
<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 class="image-desc-row">{{ scope.row.description || '暂无描述' }}</div>
</div>
@@ -104,17 +102,17 @@
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
:page-size="10"
:total="total"
layout="prev, pager, next"
@current-change="handleCurrentPageChange"
/>
</div>
</el-card>
</div>
<el-pagination
v-model:current-page="currentPage"
:page-size="10"
:total="total"
layout="prev, pager, next"
@current-change="handleCurrentPageChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 镜像详情对话框 -->
<el-dialog
@@ -201,12 +199,6 @@
</el-option>
</el-select>
</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="图标">
<div class="image-icon-upload">
<img v-if="form.image_ico" :src="mainUrl + form.image_ico" class="preview-icon" />
@@ -986,65 +978,65 @@ onMounted(() => {
<style scoped>
.container-images-container {
padding: 24px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
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;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
background: #fff;
padding: 16px 24px;
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);
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
gap: 16px;
padding: 8px;
}
.server-section {
margin-bottom: 32px;
.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;
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
border-bottom: 1px solid #e1e8ed;
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 {
margin: 0;
font-size: 18px;
font-size: 16px;
color: #303133;
font-weight: 600;
display: flex;
@@ -1055,7 +1047,7 @@ onMounted(() => {
content: '';
display: inline-block;
width: 4px;
height: 18px;
height: 16px;
background-color: #409EFF;
margin-right: 10px;
border-radius: 2px;
@@ -1066,12 +1058,6 @@ onMounted(() => {
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 {
display: flex;
align-items: center;
@@ -1118,11 +1104,12 @@ onMounted(() => {
-webkit-box-orient: vertical;
}
.pagination-container {
.pagination {
margin-top: 20px;
display: flex;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
padding: 0 16px 16px;
}
/* 详情对话框样式 */
@@ -1263,75 +1250,34 @@ onMounted(() => {
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) {
border-radius: 8px;
overflow: hidden;
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table__row:hover) {
background-color: #ecf5ff !important;
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-button--link) {
padding: 4px 8px;
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-button--link):hover {
background-color: #f0f2f5;
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;
}
:deep(.el-card__body) {
padding: 0;
}
</style>
+146 -172
View File
@@ -1,107 +1,105 @@
<template>
<div class="image-categories-container" v-loading="loading">
<!-- 页面标题 -->
<div class="page-header">
<h2>镜像分类管理</h2>
<p class="page-description">管理不同服务器下的镜像分类</p>
</div>
<!-- 操作栏 -->
<el-card class="search-card">
<el-form :inline="true" class="search-form">
<el-form-item label="服务器">
<el-select
v-model="selectedServer"
placeholder="请选择服务器"
clearable
@change="handleServerChange"
style="width: 220px"
>
<el-option
v-for="item in serverList"
:key="item.server_id"
:label="item.name"
:value="item.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="分类名称">
<el-input
v-model="searchKey"
placeholder="搜索分类名称"
clearable
@input="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleAddCategory"
: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-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" class="search-form">
<el-form-item label="服务器">
<el-select
v-model="selectedServer"
placeholder="请选择服务器"
clearable
@change="handleServerChange"
style="width: 220px"
>
<el-option
v-for="item in serverList"
:key="item.server_id"
:label="item.name"
:value="item.server_id"
/>
</el-select>
</el-form-item>
<el-form-item label="分类名称">
<el-input
v-model="searchKey"
placeholder="搜索分类名称"
clearable
@input="handleSearch"
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><search /></el-icon>搜索
</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button
type="success"
link
@click="handleEditCategory(scope.row)"
type="primary"
@click="handleAddCategory"
:disabled="!selectedServer"
>
<el-icon><edit /></el-icon>编辑
<el-icon><plus /></el-icon>添加分类
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</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
background
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="pagination"
/>
</div>
</el-card>
@@ -495,58 +493,59 @@ const getServerName = (serverId) => {
<style scoped>
.image-categories-container {
padding: 24px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
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;
flex-direction: column;
margin-bottom: 24px;
background: #fff;
padding: 16px 24px;
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;
}
.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);
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;
gap: 12px;
flex-wrap: wrap;
gap: 16px;
padding: 8px;
}
.table-card {
margin-bottom: 24px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.pagination-container {
.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;
margin-top: 16px;
padding: 0 16px 16px;
}
/* 图片上传区域样式 */
@@ -650,59 +649,34 @@ const getServerName = (serverId) => {
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) {
border-radius: 8px;
overflow: hidden;
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #606266;
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table__row:hover) {
background-color: #ecf5ff !important;
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-button--link) {
padding: 4px 8px;
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-button--link):hover {
background-color: #f0f2f5;
border-radius: 4px;
:deep(.el-card__body) {
padding: 0;
}
/* 响应式调整 */
@media (max-width: 768px) {
.image-icon-upload {
flex-direction: column;
align-items: flex-start;
}
.upload-buttons {
margin-top: 12px;
width: 100%;
}
}
</style>
</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>
<div class="image-requests-container">
<div class="page-header">
<h2>申请镜像</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>申请镜像
</el-button>
<el-button @click="handleRefresh">
<el-icon><refresh /></el-icon>刷新
</el-button>
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="镜像名称">
<el-input v-model="searchForm.name" 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="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>
<!-- 提示信息 -->
<el-alert
type="info"
show-icon
:closable="false"
class="info-alert"
>
<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-alert
type="info"
show-icon
:closable="false"
class="info-alert"
style="margin: 20px 20px 0; width: auto;"
>
<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>
<template #title>
申请的镜像需要经过安全审核审核通过后可在创建云电脑或容器时使用审核结果将通过站内信通知
</template>
</el-alert>
<!-- 分页 -->
<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
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -106,6 +106,8 @@
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
@@ -589,41 +591,58 @@ onMounted(() => {
<style scoped>
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-actions {
display: flex;
gap: 10px;
}
.info-alert {
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
.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;
}
@@ -637,12 +656,16 @@ onMounted(() => {
/* 环境变量配置样式 */
.env-vars-container {
margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
}
.env-vars-header {
display: flex;
margin-bottom: 10px;
font-weight: bold;
font-weight: 600;
color: #606266;
}
.env-vars-item {
@@ -665,17 +688,23 @@ onMounted(() => {
.add-env-btn {
margin-top: 10px;
width: 100%;
border-style: dashed;
}
/* 端口配置样式 */
.ports-container {
margin-bottom: 20px;
background-color: #f8f9fa;
padding: 16px;
border-radius: 4px;
}
.ports-header {
display: flex;
margin-bottom: 10px;
font-weight: bold;
font-weight: 600;
color: #606266;
}
.ports-item {
@@ -702,6 +731,8 @@ onMounted(() => {
.add-port-btn {
margin-top: 10px;
width: 100%;
border-style: dashed;
}
/* 详情样式 */
@@ -710,11 +741,13 @@ onMounted(() => {
}
.request-reason {
background-color: #f8f8f8;
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
white-space: pre-wrap;
color: #606266;
line-height: 1.6;
}
.review-comment {
@@ -724,5 +757,37 @@ onMounted(() => {
margin-top: 10px;
white-space: pre-wrap;
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>
<div class="announcements-container">
<div class="page-header">
<h2>官方公告</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布公告
</el-button>
</div>
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<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">
<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-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 label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<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>
</el-card>
<!-- 数据表格 -->
<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 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" />
<el-table-column prop="publishTime" label="发布时间" width="180" />
<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" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button
type="warning"
link
@click="handleChangeStatus(scope.row)"
v-if="scope.row.status !== 'offline'"
>下线</el-button>
<el-button
type="danger"
link
@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
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -102,6 +96,8 @@
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
@@ -365,32 +361,58 @@ onMounted(() => {
<style scoped>
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
.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;
}
@@ -420,4 +442,35 @@ onMounted(() => {
line-height: 1.8;
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>
<div class="news-container">
<div class="page-header">
<h2>新闻咨询</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布新闻
</el-button>
</div>
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<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">
<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">
<!-- 数据表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="newsData"
border
style="width: 100%"
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 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 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="danger" link @click="handleDelete(scope.row)">
<el-icon><delete /></el-icon>删除
</el-button>
<el-button type="primary" link @click="handleView(scope.row)">查看</el-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
<!-- 新闻详情对话框 -->
<el-dialog
@@ -400,36 +397,58 @@ onMounted(() => {
<style scoped>
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.news-list {
margin-bottom: 20px;
}
.table-card {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
.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-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;
}
@@ -487,4 +506,35 @@ onMounted(() => {
margin-right: 5px;
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>
<div class="policies-container">
<div class="page-header">
<h2>官方政策</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><plus /></el-icon>发布政策
</el-button>
</div>
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<div class="filter-content">
<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">
<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.type" placeholder="请选择政策类型" clearable>
<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
/>
</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 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 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-button>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button
type="warning"
link
@click="handleChangeStatus(scope.row)"
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
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
@@ -111,6 +105,8 @@
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</div>
</el-card>
@@ -435,32 +431,58 @@ onMounted(() => {
<style scoped>
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.table-card {
margin-bottom: 20px;
.search-form {
margin: 0;
flex: 1;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
.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;
}
@@ -495,4 +517,35 @@ onMounted(() => {
line-height: 1.8;
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" /> -->
<!-- 主要内容区域 -->
<div class="content-wrapper">
<el-tabs type="border-card" class="main-tabs">
<el-card class="main-container" shadow="never">
<el-tabs class="main-tabs">
<!-- 实例规格列表 -->
<el-tab-pane label="实例规格列表">
<div class="tab-header">
@@ -464,11 +464,11 @@
<el-table
v-loading="specLoading"
:data="spec_list"
border
stripe
style="width: 100%"
table-layout="auto"
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="name" label="规格名称" min-width="120" show-overflow-tooltip />
@@ -510,24 +510,24 @@
</template>
</el-table-column>
<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">
<div class="table-actions">
<el-tooltip content="编辑" placement="top" :hide-after="1500">
<el-button
type="primary"
circle
link
:icon="Edit"
@click="show_spec(scope.row); centerDialogVisible = true; addOrChange = false;"
/>
>编辑</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" :hide-after="1500">
<el-button
type="danger"
circle
link
:icon="Delete"
@click="deleteSpec(scope.row.plan_id)"
/>
>删除</el-button>
</el-tooltip>
</div>
</template>
@@ -562,9 +562,9 @@
<el-table
:data="user_servers"
stripe
border
style="width: 100%"
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="价格" prop="pay" width="100">
@@ -598,7 +598,7 @@
: scope.row.container_state == 4
? 'danger'
: 'info'"
effect="light"
effect="plain"
>
{{
scope.row.container_state == 0
@@ -616,11 +616,11 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="scope">
<el-button
type="primary"
size="small"
link
:icon="Setting"
@click="$router.push('/servers/container?container_id=' + scope.row.container_id)"
>
@@ -667,9 +667,9 @@
<el-table
:data="floatList"
style="width: 100%"
border
stripe
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="创建时间" min-width="160">
@@ -680,7 +680,7 @@
<el-table-column label="浮动IP" prop="floating_ip" min-width="150" />
<el-table-column label="状态" width="100" align="center">
<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 ? "已绑定" : "未绑定" }}
</el-tag>
</template>
@@ -690,10 +690,10 @@
<el-tooltip content="删除IP" placement="top" :hide-after="1500">
<el-button
type="danger"
circle
link
:icon="Delete"
@click="delFloating(scope.row.id)"
/>
>删除</el-button>
</el-tooltip>
</template>
</el-table-column>
@@ -702,7 +702,7 @@
<el-empty v-if="floatList.length === 0" description="暂无浮动IP数据" />
</el-tab-pane>
</el-tabs>
</div>
</el-card>
<!-- 添加/编辑实例规格对话框 -->
<el-dialog
@@ -2617,9 +2617,7 @@ import { ElMessageBox } from 'element-plus';
<style scoped>
.server-container {
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 120px);
padding: 0;
}
/* 页面标题区域 */
@@ -2628,6 +2626,9 @@ import { ElMessageBox } from 'element-plus';
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: #fff;
border-bottom: 1px solid #e1e8ed;
}
.page-header .left {
@@ -2638,7 +2639,7 @@ import { ElMessageBox } from 'element-plus';
.page-header .title {
margin: 0;
font-size: 24px;
font-size: 20px;
font-weight: 600;
color: #303133;
}
@@ -2647,7 +2648,8 @@ import { ElMessageBox } from 'element-plus';
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
padding: 4px 12px;
border-radius: 4px;
}
.status-dot {
@@ -2669,13 +2671,13 @@ import { ElMessageBox } from 'element-plus';
/* 返回按钮样式 */
.back-btn {
font-size: 16px;
color: #409EFF;
padding: 8px 0;
margin-right: 16px;
color: #606266;
padding: 0;
margin-right: 8px;
}
.back-btn:hover {
color: #66b1ff;
color: #409EFF;
}
/* 服务器信息卡片 */
@@ -2684,6 +2686,7 @@ import { ElMessageBox } from 'element-plus';
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
padding: 0 20px;
}
/* 服务器详细信息卡片 */
@@ -2692,16 +2695,17 @@ import { ElMessageBox } from 'element-plus';
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
padding: 0 20px;
}
.info-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
padding: 0;
overflow: hidden;
transition: all 0.3s;
border: 1px solid #ebeef5;
border: 1px solid #e1e8ed;
}
.info-card:hover {
@@ -2710,10 +2714,10 @@ import { ElMessageBox } from 'element-plus';
}
.card-title {
background-color: #f5f7fa;
background-color: #fafbfc;
padding: 12px 16px;
border-bottom: 1px solid #ebeef5;
font-size: 16px;
border-bottom: 1px solid #e1e8ed;
font-size: 15px;
font-weight: 600;
color: #303133;
display: flex;
@@ -2722,7 +2726,7 @@ import { ElMessageBox } from 'element-plus';
}
.card-title .el-icon {
font-size: 18px;
font-size: 16px;
color: #409EFF;
}
@@ -2745,32 +2749,31 @@ import { ElMessageBox } from 'element-plus';
}
.info-value {
font-size: 15px;
font-size: 14px;
color: #303133;
word-break: break-all;
}
.info-value.highlight {
font-weight: 600;
font-size: 16px;
color: #409EFF;
}
/* 硬件信息样式 - 符合整体设计风格 */
/* 硬件信息样式 */
.device-count {
color: #909399;
font-size: 14px;
font-size: 13px;
font-weight: normal;
margin-left: 8px;
}
.hardware-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
margin-bottom: 20px;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
border-bottom: 1px solid #f0f2f5;
}
.summary-item {
@@ -2780,21 +2783,21 @@ import { ElMessageBox } from 'element-plus';
.hardware-devices {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
}
.device-item {
padding: 16px;
padding: 12px;
background-color: #fafbfc;
border-radius: 6px;
border: 1px solid #e4e7ed;
border-radius: 4px;
border: 1px solid #e1e8ed;
}
.device-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
margin-bottom: 8px;
}
.device-title {
@@ -2807,57 +2810,27 @@ import { ElMessageBox } from 'element-plus';
.device-title .el-icon {
margin-right: 6px;
font-size: 16px;
font-size: 14px;
}
.device-details {
margin-top: 12px;
margin-top: 8px;
}
.detail-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 12px;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
margin-bottom: 8px;
}
.usage-progress {
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 {
margin-bottom: 20px;
margin-bottom: 16px;
}
.traffic-section:last-child {
@@ -2865,62 +2838,38 @@ import { ElMessageBox } from 'element-plus';
}
.section-title {
font-size: 14px;
font-size: 13px;
font-weight: 600;
margin-bottom: 12px;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid #ebeef5;
border-bottom: 1px solid #f0f2f5;
color: #606266;
}
.traffic-speed-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-total-grid,
.traffic-distribution {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.raw-data {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
padding: 12px;
background-color: #fafbfc;
border-radius: 6px;
border: 1px solid #e4e7ed;
border-radius: 4px;
border: 1px solid #e1e8ed;
font-family: 'Courier New', monospace;
}
.raw-data .info-value {
font-size: 13px;
font-size: 12px;
color: #606266;
}
.traffic-note {
margin-top: 8px;
}
/* 旧样式保持兼容 */
.usage-high {
color: #F56C6C;
font-weight: 600;
}
.usage-medium {
color: #E6A23C;
font-weight: 600;
}
/* 错误状态样式 */
.error-status {
color: #F56C6C;
@@ -2929,15 +2878,10 @@ import { ElMessageBox } from 'element-plus';
}
/* 主要内容区域 */
.chart-section {
margin-bottom: 24px;
}
.content-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
.main-container {
margin: 0 20px 20px 20px;
border: 1px solid #e1e8ed;
background: #ffffff;
}
.tab-header {
@@ -2945,11 +2889,13 @@ import { ElMessageBox } from 'element-plus';
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
border-bottom: 1px solid #f0f2f5;
}
.tab-title {
margin: 0;
font-size: 18px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
@@ -2969,40 +2915,67 @@ import { ElMessageBox } from 'element-plus';
gap: 8px;
}
/* 表格样式 */
.data-table {
margin-bottom: 16px;
/* 表格样式优化 */
:deep(.el-table) {
border: none;
color: #2c3e50;
}
.table-actions {
display: flex;
justify-content: center;
gap: 8px;
:deep(.el-table__header) {
background: #f8f9fa;
}
.resource-value {
white-space: nowrap;
}
.unit {
color: #909399;
font-size: 12px;
margin-left: 2px;
}
.price-tag {
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
color: #F56C6C;
font-size: 13px;
}
.time-info {
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;
}
/* 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;
}
:deep(.el-tabs__item.is-active) {
color: #409EFF;
background: #fff;
font-weight: 600;
}
:deep(.el-tabs__content) {
padding: 0;
}
/* 分页样式 */
.pagination-container {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
display: flex;
justify-content: flex-end;
}
@@ -3013,15 +2986,13 @@ import { ElMessageBox } from 'element-plus';
}
.section-title {
font-size: 16px;
font-size: 15px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
border-bottom: 1px dashed #e1e8ed;
}
.dialog-footer {
display: flex;
justify-content: space-between;
@@ -3124,27 +3095,24 @@ import { ElMessageBox } from 'element-plus';
gap: 16px;
}
.page-header .left {
flex-wrap: wrap;
}
.back-btn {
margin-right: 8px;
margin-bottom: 8px;
}
.server-info {
grid-template-columns: 1fr;
padding: 0 16px;
}
.server-detail-info {
grid-template-columns: 1fr;
padding: 0 16px;
}
.info-card.location-info {
grid-column: auto;
}
.main-container {
margin: 0 16px 16px 16px;
}
.tab-header {
flex-direction: column;
align-items: flex-start;
@@ -3159,40 +3127,5 @@ import { ElMessageBox } from 'element-plus';
width: 100%;
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>