308 lines
7.1 KiB
Vue
308 lines
7.1 KiB
Vue
<template>
|
||
<div class="key-value-editor">
|
||
<div v-if="bulkEdit" class="kv-toolbar">
|
||
<el-button type="text" size="mini" class="kv-mode-btn" @click="toggleBulkMode">
|
||
{{ bulkMode ? '键值编辑' : '批量编辑' }}
|
||
</el-button>
|
||
</div>
|
||
|
||
<div v-if="!bulkMode" class="kv-table">
|
||
<div class="kv-header" :class="rowLayoutClass">
|
||
<span v-if="showEnabled" class="col-check" />
|
||
<span class="col-key">Key</span>
|
||
<span class="col-value">Value</span>
|
||
<span v-if="showDescription" class="col-desc">Description</span>
|
||
<span class="col-action" />
|
||
</div>
|
||
<div
|
||
v-for="(item, index) in rows"
|
||
:key="index"
|
||
class="kv-row"
|
||
:class="[rowLayoutClass, { 'is-disabled': showEnabled && item.enabled === false }]"
|
||
>
|
||
<div v-if="showEnabled" class="col-check">
|
||
<el-checkbox v-model="item.enabled" />
|
||
</div>
|
||
<div class="col-key">
|
||
<el-input v-model="item.key" size="small" :placeholder="keyPlaceholder" />
|
||
</div>
|
||
<div class="col-value">
|
||
<el-input v-model="item.value" size="small" :placeholder="valuePlaceholder" />
|
||
</div>
|
||
<div v-if="showDescription" class="col-desc">
|
||
<el-input v-model="item.description" size="small" placeholder="说明" />
|
||
</div>
|
||
<div class="col-action">
|
||
<el-button
|
||
type="text"
|
||
size="small"
|
||
class="kv-delete-btn"
|
||
icon="el-icon-delete"
|
||
@click="removeRow(index)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div v-if="!rows.length" class="kv-empty">暂无参数,点击下方按钮添加或使用批量编辑</div>
|
||
</div>
|
||
|
||
<div v-else class="kv-bulk">
|
||
<el-input
|
||
v-model="bulkText"
|
||
type="textarea"
|
||
:rows="bulkRows"
|
||
:placeholder="bulkPlaceholder"
|
||
@blur="syncBulkToRows"
|
||
/>
|
||
<p v-if="bulkError" class="kv-bulk-error">{{ bulkError }}</p>
|
||
<p class="kv-bulk-hint">{{ bulkHint }}</p>
|
||
</div>
|
||
|
||
<div v-if="!bulkMode" class="kv-footer">
|
||
<el-button type="text" size="mini" icon="el-icon-plus" class="kv-add-btn" @click="addRow">
|
||
{{ addText }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { bulkTextToRows, rowsToBulkText } from '../utils/requestTemplate';
|
||
|
||
export default {
|
||
name: 'PlatformKeyValueEditor',
|
||
props: {
|
||
value: { type: Array, default: () => [] },
|
||
addText: { type: String, default: '添加参数' },
|
||
keyPlaceholder: { type: String, default: '参数名' },
|
||
valuePlaceholder: { type: String, default: '参数值' },
|
||
showEnabled: { type: Boolean, default: true },
|
||
showDescription: { type: Boolean, default: true },
|
||
bulkEdit: { type: Boolean, default: true },
|
||
bulkRows: { type: Number, default: 10 },
|
||
bulkPlaceholder: {
|
||
type: String,
|
||
default: '每行一个参数,格式 key:value\n以 # 开头的行表示禁用,例如:\n#disabledKey:value\nactiveKey:${token}',
|
||
},
|
||
bulkHint: {
|
||
type: String,
|
||
default: '每行格式为 key:value,仅按第一个冒号分割;以 # 开头的行在保存时视为禁用项。',
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
bulkMode: false,
|
||
bulkText: '',
|
||
bulkError: '',
|
||
};
|
||
},
|
||
computed: {
|
||
rows: {
|
||
get() {
|
||
return this.value;
|
||
},
|
||
set(value) {
|
||
this.$emit('input', value);
|
||
},
|
||
},
|
||
rowLayoutClass() {
|
||
return {
|
||
'no-desc': !this.showDescription,
|
||
'no-check': !this.showEnabled,
|
||
};
|
||
},
|
||
},
|
||
beforeDestroy() {
|
||
if (this._bulkTimer) {
|
||
clearTimeout(this._bulkTimer);
|
||
}
|
||
},
|
||
methods: {
|
||
/** 提交表单前调用,将批量编辑内容写回 v-model */
|
||
flushBulk() {
|
||
if (!this.bulkMode) {
|
||
return;
|
||
}
|
||
this.applyBulkText(true);
|
||
},
|
||
toggleBulkMode() {
|
||
if (this.bulkMode) {
|
||
if (!this.applyBulkText()) {
|
||
return;
|
||
}
|
||
this.bulkMode = false;
|
||
this.bulkError = '';
|
||
return;
|
||
}
|
||
this.bulkText = rowsToBulkText(this.rows);
|
||
this.bulkError = '';
|
||
this.bulkMode = true;
|
||
},
|
||
syncBulkToRows() {
|
||
if (!this.bulkMode) {
|
||
return;
|
||
}
|
||
this.applyBulkText(true);
|
||
},
|
||
applyBulkText(silent) {
|
||
const parsed = bulkTextToRows(this.bulkText);
|
||
if (!silent && this.bulkText.trim() && !parsed.length) {
|
||
this.bulkError = '未能解析出有效参数,请检查每行是否为 key:value 格式';
|
||
return false;
|
||
}
|
||
const descMap = {};
|
||
(this.rows || []).forEach(row => {
|
||
if (row && row.key) {
|
||
descMap[row.key] = row.description || '';
|
||
}
|
||
});
|
||
parsed.forEach(row => {
|
||
row.description = descMap[row.key] || '';
|
||
});
|
||
this.rows = parsed.length ? parsed : [];
|
||
this.bulkError = '';
|
||
return true;
|
||
},
|
||
addRow() {
|
||
const rows = this.rows.slice();
|
||
rows.push({ enabled: true, key: '', value: '', description: '' });
|
||
this.rows = rows;
|
||
},
|
||
removeRow(index) {
|
||
const rows = this.rows.slice();
|
||
rows.splice(index, 1);
|
||
this.rows = rows;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.key-value-editor {
|
||
width: 100%;
|
||
}
|
||
|
||
.kv-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.kv-mode-btn {
|
||
padding: 0 4px;
|
||
font-size: 12px;
|
||
color: #409eff;
|
||
}
|
||
|
||
.kv-table {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
}
|
||
|
||
.kv-header,
|
||
.kv-row {
|
||
display: grid;
|
||
grid-template-columns: 36px minmax(0, 1fr) minmax(0, 1fr) 120px 36px;
|
||
align-items: center;
|
||
column-gap: 8px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.kv-header.no-desc,
|
||
.kv-row.no-desc {
|
||
grid-template-columns: 36px minmax(0, 1fr) minmax(0, 1fr) 36px;
|
||
}
|
||
|
||
.kv-header.no-check,
|
||
.kv-row.no-check {
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 120px 36px;
|
||
}
|
||
|
||
.kv-header.no-check.no-desc,
|
||
.kv-row.no-check.no-desc {
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 36px;
|
||
}
|
||
|
||
.kv-header {
|
||
height: 36px;
|
||
background: #f5f7fa;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #909399;
|
||
}
|
||
|
||
.kv-row {
|
||
min-height: 40px;
|
||
padding-top: 4px;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
}
|
||
|
||
.kv-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.kv-row.is-disabled .col-key ::v-deep .el-input__inner,
|
||
.kv-row.is-disabled .col-value ::v-deep .el-input__inner {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.col-check {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.col-action {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.kv-delete-btn {
|
||
padding: 4px;
|
||
color: #f56c6c;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.kv-delete-btn:hover {
|
||
color: #f78989;
|
||
}
|
||
|
||
.kv-empty {
|
||
padding: 16px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.kv-footer {
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.kv-add-btn {
|
||
padding-left: 0;
|
||
color: #409eff;
|
||
}
|
||
|
||
.kv-bulk ::v-deep .el-textarea__inner {
|
||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.kv-bulk-hint {
|
||
margin: 6px 0 0;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.kv-bulk-error {
|
||
margin: 6px 0 0;
|
||
font-size: 12px;
|
||
color: #f56c6c;
|
||
}
|
||
</style> |