Files
login_task_web/src/views/settings/components/ConfigGroup.vue

410 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">
<template v-if="config.configType === 'JSON'">
<el-input
v-model="localValues[config.configKey]"
type="textarea"
:rows="3"
@blur="handleValueChange(config)"
placeholder="请输入有效的JSON格式"
/>
</template>
<template v-else-if="config.configType === 'SELECT'">
<el-select
v-model="localValues[config.configKey]"
placeholder="请选择"
@change="() => handleValueChange(config)"
clearable
>
<el-option
v-for="opt in getSelectOptions(config)"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</template>
<template v-else>
<el-input
v-model="localValues[config.configKey]"
:type="config.configType === 'INTEGER' ? 'number' : 'text'"
@blur="handleValueChange(config)"
@keyup.enter="handleValueChange(config)"
:placeholder="getValuePlaceholder(config.configType)"
/>
</template>
</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',
SELECT: '下拉选择'
}
return typeMap[type] || type
}
function getValuePlaceholder(type) {
switch (type) {
case 'INTEGER':
return '请输入整数'
case 'BOOLEAN':
return '请输入 true 或 false'
case 'SELECT':
return '请选择'
case 'STRING':
return '请输入字符串值'
default:
return '请输入配置值'
}
}
// 返回对应配置项的下拉选项
function getSelectOptions(config) {
// 特殊处理 user.progress_display_format
if (config.configKey === 'user.progress_display_format') {
return [
{ label: '50/100', value: '1' },
{ label: '差50', value: '2' }
]
}
// 如果后端提供了 options 字段JSON 字符串或数组),做兼容
if (config.options) {
try {
const parsed = typeof config.options === 'string' ? JSON.parse(config.options) : config.options
// 期望形如 [{ label, value }] 或 { value: label }
if (Array.isArray(parsed)) return parsed
if (parsed && typeof parsed === 'object') {
return Object.entries(parsed).map(([value, label]) => ({ label, value }))
}
} catch (e) {
// ignore parse error
}
}
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>