重构Play.vue界面,优化标签页和状态显示,添加游戏截图展示区域,更新状态管理逻辑,提升用户体验。
This commit is contained in:
101
src/api/config.js
Normal file
101
src/api/config.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import http from '../plugins/http'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统配置管理API
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 获取配置列表
|
||||||
|
export function getConfigList(params) {
|
||||||
|
return http.get('/api/admin/config/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据键获取配置
|
||||||
|
export function getConfigByKey(configKey) {
|
||||||
|
return http.get(`/api/admin/config/key/${configKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型获取配置
|
||||||
|
export function getConfigByType(configType) {
|
||||||
|
return http.get(`/api/admin/config/type/${configType}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建配置
|
||||||
|
export function createConfig(data) {
|
||||||
|
return http.post('/api/admin/config', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
export function updateConfig(id, data) {
|
||||||
|
return http.put(`/api/admin/config/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置
|
||||||
|
export function deleteConfig(id) {
|
||||||
|
return http.delete(`/api/admin/config/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据键删除配置
|
||||||
|
export function deleteConfigByKey(configKey) {
|
||||||
|
return http.delete(`/api/admin/config/key/${configKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据键快速更新配置值
|
||||||
|
export function updateConfigValue(configKey, value) {
|
||||||
|
return http.put(`/api/admin/config/key/${configKey}`, value, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新配置
|
||||||
|
export function batchUpdateConfigs(configs) {
|
||||||
|
return http.post('/api/admin/config/batch', { configs })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取链接默认配置
|
||||||
|
export function getLinkDefaults() {
|
||||||
|
return http.get('/api/admin/config/link/defaults')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取脚本配置
|
||||||
|
export function getScriptConfig() {
|
||||||
|
return http.get('/api/admin/config/script/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户端配置
|
||||||
|
export function getUserConfig() {
|
||||||
|
return http.get('/api/admin/config/user/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置值
|
||||||
|
export function validateConfigValue(configKey, value, configType) {
|
||||||
|
return http.post('/api/admin/config/validate', {
|
||||||
|
configKey,
|
||||||
|
value,
|
||||||
|
configType
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置配置到默认值
|
||||||
|
export function resetConfig(configKey) {
|
||||||
|
return http.post(`/api/admin/config/reset/${configKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出配置
|
||||||
|
export function exportConfigs() {
|
||||||
|
return http.get('/api/admin/config/export', {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入配置
|
||||||
|
export function importConfigs(file) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return http.post('/api/admin/config/import', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -84,61 +84,50 @@
|
|||||||
|
|
||||||
<!-- 二界面 -->
|
<!-- 二界面 -->
|
||||||
<div v-else-if="state.status === 'LOGGED_IN'" class="game-page">
|
<div v-else-if="state.status === 'LOGGED_IN'" class="game-page">
|
||||||
<div class="game-header">
|
<!-- 顶部标签页 -->
|
||||||
<div class="browser-bar">
|
<div class="tab-header">
|
||||||
<span class="url">{{ getCurrentUrl() }}</span>
|
<div class="tab-item active">代练大区</div>
|
||||||
</div>
|
<div class="tab-item status-tab">状态</div>
|
||||||
|
<div class="tab-item target-tab">目标点数</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-header">
|
||||||
|
<div class="tab-item active" v-if="state.region === 'Q'">QQ</div>
|
||||||
|
<div class="tab-item active" v-if="state.region === 'V'">微信</div>
|
||||||
|
<div class="tab-item status-tab">状态</div>
|
||||||
|
<div class="tab-item target-tab">{{ state.totalPoints || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
<div class="game-content">
|
<!-- 状态提示 -->
|
||||||
<!-- 大厅选择显示 -->
|
<div class="status-message">
|
||||||
<div class="hall-info">
|
正在代练中,期间请勿操号,耐心等待代练完成......
|
||||||
<span class="hall-label">选择大厅:</span>
|
|
||||||
<span class="hall-value">{{ getRegionName() }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 状态显示 -->
|
|
||||||
<div class="status-info">
|
|
||||||
<span class="status-label">状态:</span>
|
|
||||||
<span class="status-value" :class="getStatusClass()">{{ getGameStatus() }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 目标点数进度 -->
|
<!-- 游戏截图展示区域 -->
|
||||||
<div class="progress-section">
|
<div class="image-gallery">
|
||||||
<div class="progress-header">
|
<div class="image-item">
|
||||||
<span class="progress-label">目标点数</span>
|
<img src="http://36.138.184.60:12345/f1/首次主页.png" alt="首次主页" class="game-image" />
|
||||||
<span class="progress-value">{{ state.currentPoints || 0 }}/{{ state.totalPoints || 1000 }}</span>
|
<div class="image-label">首次主页</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar">
|
<div class="image-item">
|
||||||
<div class="progress-fill" :style="{ width: getProgressPercent() + '%' }"></div>
|
<img src="http://36.138.184.60:12345/f1/首次赏金.png" alt="首次赏金" class="game-image" style="transform: rotate(-90deg);" />
|
||||||
|
<div class="image-label">首次赏金</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-item">
|
||||||
|
<img src="http://36.138.184.60:12345/f1/中途赏金.png" alt="中途赏金" class="game-image" />
|
||||||
|
<div class="image-label">中途赏金</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-item">
|
||||||
|
<img src="http://36.138.184.60:12345/f1/结束赏金.png" alt="结束赏金" class="game-image" />
|
||||||
|
<div class="image-label">结束赏金</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 游戏截图区域 -->
|
<!-- 底部状态显示 -->
|
||||||
<div class="game-images" v-if="state.assets">
|
<div class="bottom-status">
|
||||||
<div class="image-container">
|
ss{{ state.currentPoints || 0 }}
|
||||||
<img
|
|
||||||
v-if="getCurrentGameImage()"
|
|
||||||
:src="getCurrentGameImage()"
|
|
||||||
class="game-screenshot"
|
|
||||||
alt="游戏截图"
|
|
||||||
/>
|
|
||||||
<div v-else class="placeholder-image">
|
|
||||||
<p>正在上号中,请稍等...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 操作按钮区域 -->
|
|
||||||
<div class="game-actions">
|
|
||||||
<button class="action-btn completed-btn">已完成</button>
|
|
||||||
<button class="action-btn current-btn">进行中</button>
|
|
||||||
<button class="action-btn pending-btn">未开始</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部信息 -->
|
|
||||||
<div class="bottom-info">
|
|
||||||
<p class="safe-text">安全可靠的代练服务</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,10 +288,18 @@ export default {
|
|||||||
state.status = 'LOGGED_IN'
|
state.status = 'LOGGED_IN'
|
||||||
state.assets = gameData.assets
|
state.assets = gameData.assets
|
||||||
|
|
||||||
// 从游戏接口数据中更新总点数和当前进度
|
// 更新区域信息
|
||||||
if (gameData.assets && gameData.assets.totalPoints) {
|
if (gameData.region) {
|
||||||
|
state.region = gameData.region
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从游戏接口数据中更新总点数 - 优先使用根级别的totalPoints
|
||||||
|
if (gameData.totalPoints) {
|
||||||
|
state.totalPoints = gameData.totalPoints
|
||||||
|
} else if (gameData.assets && gameData.assets.totalPoints) {
|
||||||
state.totalPoints = gameData.assets.totalPoints
|
state.totalPoints = gameData.assets.totalPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化当前点数为0(代表刚开始)
|
// 初始化当前点数为0(代表刚开始)
|
||||||
state.currentPoints = 0
|
state.currentPoints = 0
|
||||||
|
|
||||||
@@ -329,7 +326,12 @@ export default {
|
|||||||
state.region = data.region
|
state.region = data.region
|
||||||
state.assets = data.assets
|
state.assets = data.assets
|
||||||
|
|
||||||
// 如果有游戏数据,更新点数信息
|
// 更新点数信息 - totalPoints 在根级别
|
||||||
|
if (data.totalPoints) {
|
||||||
|
state.totalPoints = data.totalPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有游戏数据,也检查assets中的点数信息
|
||||||
if (data.assets && data.assets.totalPoints) {
|
if (data.assets && data.assets.totalPoints) {
|
||||||
state.totalPoints = data.assets.totalPoints
|
state.totalPoints = data.assets.totalPoints
|
||||||
// 如果没有当前进度,初始化为0
|
// 如果没有当前进度,初始化为0
|
||||||
@@ -939,178 +941,194 @@ export default {
|
|||||||
background: white;
|
background: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-header {
|
/* 标签页头部 */
|
||||||
background: #f8f9fa;
|
.tab-header {
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-bar {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-content {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hall-info, .status-info {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
background: #f8f9fa;
|
||||||
align-items: center;
|
|
||||||
padding: 12px 0;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid #e9ecef;
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hall-label, .status-label {
|
.tab-item {
|
||||||
color: #666;
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
|
||||||
|
|
||||||
.hall-value {
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-value {
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-completed {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-playing {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-idle {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-section {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-label {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-value {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
height: 8px;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-images {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 12px;
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-screenshot {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 300px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-image {
|
|
||||||
color: #666;
|
color: #666;
|
||||||
text-align: center;
|
border-bottom: 2px solid transparent;
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed-btn {
|
.tab-item.active {
|
||||||
background: #28a745;
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border-bottom-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.status-tab {
|
||||||
|
background: #2196F3;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-btn {
|
.tab-item.target-tab {
|
||||||
background: #ffc107;
|
color: #f44336;
|
||||||
color: #212529;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pending-btn {
|
/* 标签内容区域 */
|
||||||
background: #6c757d;
|
.tab-content {
|
||||||
color: white;
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-info {
|
/* 标签按钮区域 */
|
||||||
text-align: center;
|
.tab-buttons {
|
||||||
padding: 20px 0;
|
display: flex;
|
||||||
border-top: 1px solid #e9ecef;
|
gap: 12px;
|
||||||
margin-top: auto;
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.safe-text {
|
.tab-btn {
|
||||||
color: #666;
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.qq-active {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.ios-btn {
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-points {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #f44336;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态消息 */
|
||||||
|
.status-message {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 功能按钮网格 */
|
||||||
|
.function-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 12px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 游戏截图展示区域 */
|
||||||
|
.image-gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 150px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部状态显示 */
|
||||||
|
.bottom-status {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
margin: 0 -16px -16px -16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 刷新等待界面 */
|
/* 刷新等待界面 */
|
||||||
@@ -1240,8 +1258,69 @@ export default {
|
|||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-actions {
|
/* 移动端二界面优化 */
|
||||||
|
.tab-item {
|
||||||
|
padding: 10px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-points {
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-item {
|
||||||
|
padding: 16px 8px;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-text {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端图片画廊优化 */
|
||||||
|
.image-gallery {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-label {
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,549 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-card shadow="hover">
|
<div class="system-config">
|
||||||
<template #header>系统设置</template>
|
<!-- 页面标题 -->
|
||||||
<p>系统设置空页面(参数配置、权限、主题)。</p>
|
<div class="page-header">
|
||||||
</el-card>
|
<h2>系统参数配置</h2>
|
||||||
|
<p>管理系统运行参数和配置项</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="handleAdd" v-permission="'setting:manage'">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加配置
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" @click="handleBatchSave" :disabled="!hasChanges">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
批量保存
|
||||||
|
</el-button>
|
||||||
|
<el-button type="warning" @click="handleExport">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
导出配置
|
||||||
|
</el-button>
|
||||||
|
<el-button type="info" @click="handleImport">
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
|
导入配置
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleRefresh">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配置分类标签页 -->
|
||||||
|
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||||
|
<el-tab-pane label="链接配置" name="link">
|
||||||
|
<config-group
|
||||||
|
:configs="linkConfigs"
|
||||||
|
@update="handleConfigUpdate"
|
||||||
|
@delete="handleConfigDelete"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="脚本配置" name="script">
|
||||||
|
<config-group
|
||||||
|
:configs="scriptConfigs"
|
||||||
|
@update="handleConfigUpdate"
|
||||||
|
@delete="handleConfigDelete"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="用户端配置" name="user">
|
||||||
|
<config-group
|
||||||
|
:configs="userConfigs"
|
||||||
|
@update="handleConfigUpdate"
|
||||||
|
@delete="handleConfigDelete"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="全部配置" name="all">
|
||||||
|
<config-table
|
||||||
|
:configs="allConfigs"
|
||||||
|
:loading="loading"
|
||||||
|
@update="handleConfigUpdate"
|
||||||
|
@delete="handleConfigDelete"
|
||||||
|
@edit="handleEdit"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 添加/编辑配置对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="600px"
|
||||||
|
@close="handleDialogClose"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="120px"
|
||||||
|
label-position="right"
|
||||||
|
>
|
||||||
|
<el-form-item label="配置键" prop="configKey">
|
||||||
|
<el-input
|
||||||
|
v-model="form.configKey"
|
||||||
|
placeholder="请输入配置键,如:link.default_quantity"
|
||||||
|
:disabled="isEdit"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置值" prop="configValue">
|
||||||
|
<el-input
|
||||||
|
v-if="form.configType !== 'JSON'"
|
||||||
|
v-model="form.configValue"
|
||||||
|
:placeholder="getValuePlaceholder(form.configType)"
|
||||||
|
:type="form.configType === 'INTEGER' ? 'number' : 'text'"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="form.configValue"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入有效的JSON格式"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置类型" prop="configType">
|
||||||
|
<el-select v-model="form.configType" placeholder="请选择配置类型">
|
||||||
|
<el-option label="字符串" value="STRING" />
|
||||||
|
<el-option label="整数" value="INTEGER" />
|
||||||
|
<el-option label="布尔值" value="BOOLEAN" />
|
||||||
|
<el-option label="JSON" value="JSON" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="form.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="请输入配置描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="系统配置">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.isSystem"
|
||||||
|
active-text="是"
|
||||||
|
inactive-text="否"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">系统配置项通常不建议删除</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 导入文件对话框 -->
|
||||||
|
<el-dialog v-model="importDialogVisible" title="导入配置" width="400px">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
accept=".json"
|
||||||
|
@change="handleFileChange"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将JSON文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
只能上传json文件,且不超过500kb
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="importDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleImportConfirm" :loading="importLoading">
|
||||||
|
导入
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Check,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
Refresh,
|
||||||
|
UploadFilled
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getConfigList,
|
||||||
|
createConfig,
|
||||||
|
updateConfig,
|
||||||
|
deleteConfig,
|
||||||
|
batchUpdateConfigs,
|
||||||
|
exportConfigs,
|
||||||
|
importConfigs,
|
||||||
|
getLinkDefaults,
|
||||||
|
getScriptConfig,
|
||||||
|
getUserConfig
|
||||||
|
} from '@/api/config'
|
||||||
|
import ConfigGroup from './components/ConfigGroup.vue'
|
||||||
|
import ConfigTable from './components/ConfigTable.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const activeTab = ref('link')
|
||||||
|
const allConfigs = ref([])
|
||||||
|
const changedConfigs = ref(new Map())
|
||||||
|
|
||||||
|
// 对话框相关
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const importDialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const importLoading = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const uploadRef = ref()
|
||||||
|
const uploadFile = ref(null)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
configKey: '',
|
||||||
|
configValue: '',
|
||||||
|
configType: 'STRING',
|
||||||
|
description: '',
|
||||||
|
isSystem: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
configKey: [
|
||||||
|
{ required: true, message: '请输入配置键', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 100, message: '配置键长度在 2 到 100 个字符', trigger: 'blur' },
|
||||||
|
{ pattern: /^[a-zA-Z][a-zA-Z0-9_.]*$/, message: '配置键只能包含字母、数字、点和下划线,且以字母开头', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
configValue: [
|
||||||
|
{ required: true, message: '请输入配置值', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
configType: [
|
||||||
|
{ required: true, message: '请选择配置类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
{ max: 500, message: '描述长度不能超过 500 个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const dialogTitle = computed(() => isEdit.value ? '编辑配置' : '添加配置')
|
||||||
|
|
||||||
|
const hasChanges = computed(() => changedConfigs.value.size > 0)
|
||||||
|
|
||||||
|
const linkConfigs = computed(() => {
|
||||||
|
return allConfigs.value.filter(config => config.configKey.startsWith('link.'))
|
||||||
|
})
|
||||||
|
|
||||||
|
const scriptConfigs = computed(() => {
|
||||||
|
return allConfigs.value.filter(config => config.configKey.startsWith('script.'))
|
||||||
|
})
|
||||||
|
|
||||||
|
const userConfigs = computed(() => {
|
||||||
|
return allConfigs.value.filter(config => config.configKey.startsWith('user.'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
loadConfigs()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
async function loadConfigs() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await getConfigList({ page: 1, size: 1000 })
|
||||||
|
allConfigs.value = response.data?.items || []
|
||||||
|
changedConfigs.value.clear()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败:', error)
|
||||||
|
ElMessage.error('加载配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabChange(tabName) {
|
||||||
|
activeTab.value = tabName
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
isEdit.value = false
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(config) {
|
||||||
|
isEdit.value = true
|
||||||
|
form.id = config.id
|
||||||
|
form.configKey = config.configKey
|
||||||
|
form.configValue = config.configValue
|
||||||
|
form.configType = config.configType
|
||||||
|
form.description = config.description
|
||||||
|
form.isSystem = config.isSystem
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.id = null
|
||||||
|
form.configKey = ''
|
||||||
|
form.configValue = ''
|
||||||
|
form.configType = 'STRING'
|
||||||
|
form.description = ''
|
||||||
|
form.isSystem = false
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDialogClose() {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
|
||||||
|
// 验证配置值格式
|
||||||
|
if (!validateConfigValue(form.configValue, form.configType)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
configKey: form.configKey,
|
||||||
|
configValue: form.configValue,
|
||||||
|
configType: form.configType,
|
||||||
|
description: form.description,
|
||||||
|
isSystem: form.isSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
await updateConfig(form.id, data)
|
||||||
|
ElMessage.success('更新配置成功')
|
||||||
|
} else {
|
||||||
|
await createConfig(data)
|
||||||
|
ElMessage.success('添加配置成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = false
|
||||||
|
await loadConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交配置失败:', error)
|
||||||
|
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConfigValue(value, type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'INTEGER':
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
ElMessage.error('整数类型的值必须是数字')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'BOOLEAN':
|
||||||
|
if (!['true', 'false'].includes(value.toLowerCase())) {
|
||||||
|
ElMessage.error('布尔类型的值必须是 true 或 false')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'JSON':
|
||||||
|
try {
|
||||||
|
JSON.parse(value)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('JSON类型的值必须是有效的JSON格式')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValuePlaceholder(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'INTEGER':
|
||||||
|
return '请输入整数,如:100'
|
||||||
|
case 'BOOLEAN':
|
||||||
|
return '请输入 true 或 false'
|
||||||
|
case 'STRING':
|
||||||
|
return '请输入字符串值'
|
||||||
|
default:
|
||||||
|
return '请输入配置值'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfigUpdate(config) {
|
||||||
|
changedConfigs.value.set(config.configKey, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfigDelete(config) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除配置项 "${config.configKey}" 吗?`,
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await deleteConfig(config.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await loadConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除配置失败:', error)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchSave() {
|
||||||
|
if (changedConfigs.value.size === 0) {
|
||||||
|
ElMessage.warning('没有需要保存的更改')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configs = Array.from(changedConfigs.value.values())
|
||||||
|
await batchUpdateConfigs(configs)
|
||||||
|
ElMessage.success(`成功保存 ${configs.length} 个配置项`)
|
||||||
|
changedConfigs.value.clear()
|
||||||
|
await loadConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量保存失败:', error)
|
||||||
|
ElMessage.error('批量保存失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
const response = await exportConfigs()
|
||||||
|
const blob = new Blob([response.data], { type: 'application/json' })
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `system-config-${new Date().toISOString().split('T')[0]}.json`
|
||||||
|
link.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('导出成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出失败:', error)
|
||||||
|
ElMessage.error('导出失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImport() {
|
||||||
|
importDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileChange(file) {
|
||||||
|
uploadFile.value = file.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportConfirm() {
|
||||||
|
if (!uploadFile.value) {
|
||||||
|
ElMessage.warning('请选择要导入的文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
importLoading.value = true
|
||||||
|
await importConfigs(uploadFile.value)
|
||||||
|
ElMessage.success('导入成功')
|
||||||
|
importDialogVisible.value = false
|
||||||
|
uploadFile.value = null
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
await loadConfigs()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入失败:', error)
|
||||||
|
ElMessage.error('导入失败')
|
||||||
|
} finally {
|
||||||
|
importLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
loadConfigs()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.system-config {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-demo {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
362
src/views/settings/components/ConfigGroup.vue
Normal file
362
src/views/settings/components/ConfigGroup.vue
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
<template>
|
||||||
|
<div class="config-group">
|
||||||
|
<el-card v-for="config in configs" :key="config.id" class="config-card" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="config-info">
|
||||||
|
<h4 class="config-key">{{ config.configKey }}</h4>
|
||||||
|
<span class="config-type" :class="`type-${config.configType.toLowerCase()}`">
|
||||||
|
{{ getTypeLabel(config.configType) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="handleEdit(config)"
|
||||||
|
v-permission="'setting:manage'"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="handleDelete(config)"
|
||||||
|
v-permission="'setting:manage'"
|
||||||
|
:disabled="config.isSystem"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="config-content">
|
||||||
|
<div class="config-description">
|
||||||
|
<span class="label">描述:</span>
|
||||||
|
<span class="value">{{ config.description || '无描述' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-value-section">
|
||||||
|
<div class="value-label">
|
||||||
|
<span class="label">配置值:</span>
|
||||||
|
<el-tag v-if="config.isSystem" type="warning" size="small">系统配置</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-input">
|
||||||
|
<el-input
|
||||||
|
v-if="config.configType !== 'JSON'"
|
||||||
|
v-model="localValues[config.configKey]"
|
||||||
|
:type="config.configType === 'INTEGER' ? 'number' : 'text'"
|
||||||
|
@blur="handleValueChange(config)"
|
||||||
|
@keyup.enter="handleValueChange(config)"
|
||||||
|
:placeholder="getValuePlaceholder(config.configType)"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="localValues[config.configKey]"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
@blur="handleValueChange(config)"
|
||||||
|
placeholder="请输入有效的JSON格式"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasChanged(config)" class="value-status">
|
||||||
|
<el-tag type="success" size="small">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
已修改
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">创建时间:</span>
|
||||||
|
<span class="value">{{ formatDate(config.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">更新时间:</span>
|
||||||
|
<span class="value">{{ formatDate(config.updatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-empty v-if="configs.length === 0" description="暂无配置项" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Edit } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
configs: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'delete'])
|
||||||
|
|
||||||
|
const localValues = reactive({})
|
||||||
|
const originalValues = reactive({})
|
||||||
|
|
||||||
|
// 监听props变化,初始化本地值
|
||||||
|
watch(() => props.configs, (newConfigs) => {
|
||||||
|
newConfigs.forEach(config => {
|
||||||
|
localValues[config.configKey] = config.configValue
|
||||||
|
originalValues[config.configKey] = config.configValue
|
||||||
|
})
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.configs.forEach(config => {
|
||||||
|
localValues[config.configKey] = config.configValue
|
||||||
|
originalValues[config.configKey] = config.configValue
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTypeLabel(type) {
|
||||||
|
const typeMap = {
|
||||||
|
STRING: '字符串',
|
||||||
|
INTEGER: '整数',
|
||||||
|
BOOLEAN: '布尔值',
|
||||||
|
JSON: 'JSON'
|
||||||
|
}
|
||||||
|
return typeMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValuePlaceholder(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'INTEGER':
|
||||||
|
return '请输入整数'
|
||||||
|
case 'BOOLEAN':
|
||||||
|
return '请输入 true 或 false'
|
||||||
|
case 'STRING':
|
||||||
|
return '请输入字符串值'
|
||||||
|
default:
|
||||||
|
return '请输入配置值'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasChanged(config) {
|
||||||
|
return localValues[config.configKey] !== originalValues[config.configKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleValueChange(config) {
|
||||||
|
const newValue = localValues[config.configKey]
|
||||||
|
|
||||||
|
// 验证配置值
|
||||||
|
if (!validateConfigValue(newValue, config.configType)) {
|
||||||
|
// 验证失败,恢复原值
|
||||||
|
localValues[config.configKey] = originalValues[config.configKey]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果值有变化,触发更新事件
|
||||||
|
if (hasChanged(config)) {
|
||||||
|
emit('update', {
|
||||||
|
...config,
|
||||||
|
configValue: newValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConfigValue(value, type) {
|
||||||
|
if (!value && value !== '0' && value !== 'false') {
|
||||||
|
ElMessage.error('配置值不能为空')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'INTEGER':
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
ElMessage.error('整数类型的值必须是数字')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'BOOLEAN':
|
||||||
|
if (!['true', 'false'].includes(value.toLowerCase())) {
|
||||||
|
ElMessage.error('布尔类型的值必须是 true 或 false')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'JSON':
|
||||||
|
try {
|
||||||
|
JSON.parse(value)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('JSON类型的值必须是有效的JSON格式')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(config) {
|
||||||
|
// 这里可以触发编辑事件,由父组件处理
|
||||||
|
console.log('编辑配置:', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(config) {
|
||||||
|
emit('delete', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.config-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-type {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-string {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
color: #0ea5e9;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-integer {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-boolean {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-json {
|
||||||
|
background-color: #fdf2f8;
|
||||||
|
color: #ec4899;
|
||||||
|
border: 1px solid #fbcfe8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-input {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #303133;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
394
src/views/settings/components/ConfigTable.vue
Normal file
394
src/views/settings/components/ConfigTable.vue
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
<template>
|
||||||
|
<div class="config-table">
|
||||||
|
<el-table
|
||||||
|
:data="configs"
|
||||||
|
:loading="loading"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
height="600"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="50" />
|
||||||
|
|
||||||
|
<el-table-column prop="configKey" label="配置键" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="config-key-cell">
|
||||||
|
<span class="key-text">{{ row.configKey }}</span>
|
||||||
|
<el-tag v-if="row.isSystem" type="warning" size="small">系统</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="configValue" label="配置值" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="config-value-cell">
|
||||||
|
<el-input
|
||||||
|
v-if="row.configType !== 'JSON'"
|
||||||
|
v-model="localValues[row.configKey]"
|
||||||
|
:type="row.configType === 'INTEGER' ? 'number' : 'text'"
|
||||||
|
size="small"
|
||||||
|
@blur="handleValueChange(row)"
|
||||||
|
@keyup.enter="handleValueChange(row)"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="localValues[row.configKey]"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
size="small"
|
||||||
|
@blur="handleValueChange(row)"
|
||||||
|
/>
|
||||||
|
<div v-if="hasChanged(row)" class="value-changed">
|
||||||
|
<el-icon color="#67c23a"><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="configType" label="类型" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getTypeTagType(row.configType)" size="small">
|
||||||
|
{{ getTypeLabel(row.configType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="description-text">{{ row.description || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="updatedAt" label="更新时间" width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="time-text">{{ formatDate(row.updatedAt) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="handleEdit(row)"
|
||||||
|
v-permission="'setting:manage'"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
v-permission="'setting:manage'"
|
||||||
|
:disabled="row.isSystem"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 批量操作栏 -->
|
||||||
|
<div v-if="selectedConfigs.length > 0" class="batch-actions">
|
||||||
|
<div class="selected-info">
|
||||||
|
已选择 {{ selectedConfigs.length }} 项
|
||||||
|
</div>
|
||||||
|
<div class="batch-buttons">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleBatchDelete"
|
||||||
|
:disabled="selectedConfigs.some(c => c.isSystem)"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="clearSelection">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
configs: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'delete', 'edit'])
|
||||||
|
|
||||||
|
const localValues = reactive({})
|
||||||
|
const originalValues = reactive({})
|
||||||
|
const selectedConfigs = ref([])
|
||||||
|
|
||||||
|
// 监听props变化,初始化本地值
|
||||||
|
watch(() => props.configs, (newConfigs) => {
|
||||||
|
newConfigs.forEach(config => {
|
||||||
|
localValues[config.configKey] = config.configValue
|
||||||
|
originalValues[config.configKey] = config.configValue
|
||||||
|
})
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.configs.forEach(config => {
|
||||||
|
localValues[config.configKey] = config.configValue
|
||||||
|
originalValues[config.configKey] = config.configValue
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTypeLabel(type) {
|
||||||
|
const typeMap = {
|
||||||
|
STRING: '字符串',
|
||||||
|
INTEGER: '整数',
|
||||||
|
BOOLEAN: '布尔值',
|
||||||
|
JSON: 'JSON'
|
||||||
|
}
|
||||||
|
return typeMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeTagType(type) {
|
||||||
|
const typeMap = {
|
||||||
|
STRING: '',
|
||||||
|
INTEGER: 'success',
|
||||||
|
BOOLEAN: 'warning',
|
||||||
|
JSON: 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[type] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasChanged(config) {
|
||||||
|
return localValues[config.configKey] !== originalValues[config.configKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleValueChange(config) {
|
||||||
|
const newValue = localValues[config.configKey]
|
||||||
|
|
||||||
|
// 验证配置值
|
||||||
|
if (!validateConfigValue(newValue, config.configType)) {
|
||||||
|
// 验证失败,恢复原值
|
||||||
|
localValues[config.configKey] = originalValues[config.configKey]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果值有变化,触发更新事件
|
||||||
|
if (hasChanged(config)) {
|
||||||
|
emit('update', {
|
||||||
|
...config,
|
||||||
|
configValue: newValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConfigValue(value, type) {
|
||||||
|
if (!value && value !== '0' && value !== 'false') {
|
||||||
|
ElMessage.error('配置值不能为空')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'INTEGER':
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
ElMessage.error('整数类型的值必须是数字')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'BOOLEAN':
|
||||||
|
if (!['true', 'false'].includes(value.toLowerCase())) {
|
||||||
|
ElMessage.error('布尔类型的值必须是 true 或 false')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'JSON':
|
||||||
|
try {
|
||||||
|
JSON.parse(value)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('JSON类型的值必须是有效的JSON格式')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(config) {
|
||||||
|
emit('edit', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(config) {
|
||||||
|
emit('delete', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(selection) {
|
||||||
|
selectedConfigs.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
const systemConfigs = selectedConfigs.value.filter(c => c.isSystem)
|
||||||
|
if (systemConfigs.length > 0) {
|
||||||
|
ElMessage.warning('选择的配置中包含系统配置,无法删除')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除选中的 ${selectedConfigs.value.length} 个配置项吗?`,
|
||||||
|
'确认批量删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 依次删除选中的配置
|
||||||
|
for (const config of selectedConfigs.value) {
|
||||||
|
emit('delete', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedConfigs.value = []
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedConfigs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.config-table {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-text {
|
||||||
|
flex: 1;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value-cell {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-changed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-top: none;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table th) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table td) {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 0 1px #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper:hover) {
|
||||||
|
box-shadow: 0 0 0 1px #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper.is-focus) {
|
||||||
|
box-shadow: 0 0 0 1px #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user