refactor: 优化配置管理页面输入控件、撤销功能和页面结构
修复数字/JSON/switch字段的输入类型,重置修改改为真正的撤销, 移除纯展示panel精简页面,保存改为并行提升效率。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
276a3c0017
commit
2ef7624772
@ -33,7 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header"><h3>规则参数</h3><span>已联调 `/api/configs/{config_key}` 读取与保存</span></div>
|
<div class="panel-header"><h3>规则参数</h3></div>
|
||||||
|
<p class="panel-note">配置联动提醒中心与报表模块。分组:提醒规则(物流超时/欠款/沉默客户)、权限规则(脱敏/视图隔离)、利润规则(阈值/提成)。</p>
|
||||||
<div class="config-grid">
|
<div class="config-grid">
|
||||||
<article v-for="item in configs" :key="item.key" class="config-card">
|
<article v-for="item in configs" :key="item.key" class="config-card">
|
||||||
<div class="config-card-top">
|
<div class="config-card-top">
|
||||||
@ -42,7 +43,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>{{ item.description }}</p>
|
<p>{{ item.description }}</p>
|
||||||
<div class="input-grid">
|
<div class="input-grid">
|
||||||
<label v-if="item.type !== 'switch'"><span>值</span><input v-model="item.value" type="text" @input="markDirty(item)" /></label>
|
<label v-if="item.type === 'number'"><span>值</span><input v-model="item.value" type="number" min="0" step="1" @input="markDirty(item)" /></label>
|
||||||
|
<label v-else-if="item.type === 'json'" class="full-width"><span>值</span><textarea v-model="item.value" rows="3" @input="markDirty(item)"></textarea></label>
|
||||||
|
<div v-else-if="item.type === 'switch'" class="switch-group">
|
||||||
|
<span class="switch-label">值</span>
|
||||||
|
<div class="switch-options">
|
||||||
|
<label :class="{ selected: item.value === 'after_ship' }"><input type="radio" v-model="item.value" value="after_ship" @change="markDirty(item)" /> 发货后记账</label>
|
||||||
|
<label :class="{ selected: item.value === 'after_deliver' }"><input type="radio" v-model="item.value" value="after_deliver" @change="markDirty(item)" /> 送达后记账</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<label><span>配置名</span><input v-model="item.name" type="text" @input="markDirty(item)" /></label>
|
<label><span>配置名</span><input v-model="item.name" type="text" @input="markDirty(item)" /></label>
|
||||||
<label><span>备注</span><input v-model="item.remark" type="text" @input="markDirty(item)" /></label>
|
<label><span>备注</span><input v-model="item.remark" type="text" @input="markDirty(item)" /></label>
|
||||||
</div>
|
</div>
|
||||||
@ -50,21 +59,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<button class="ghost-btn" :disabled="saving" @click="reloadConfig">重新加载</button>
|
<button class="ghost-btn" :disabled="saving" @click="reloadConfig">重新加载</button>
|
||||||
<button class="ghost-btn" :disabled="saving" @click="resetDirty">重置修改状态</button>
|
<button class="ghost-btn" :disabled="saving" @click="resetDirty">撤销修改</button>
|
||||||
<button class="primary-btn" :disabled="saving" @click="saveAll">{{ saving ? '保存中...' : '保存全部配置' }}</button>
|
<button class="primary-btn" :disabled="saving" @click="saveAll">{{ saving ? '保存中...' : '保存全部配置' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-header"><h3>配置分组</h3><span>按业务板块查看</span></div>
|
|
||||||
<div class="group-grid">
|
|
||||||
<article v-for="group in groups" :key="group.title" class="group-card">
|
|
||||||
<h4>{{ group.title }}</h4>
|
|
||||||
<p>{{ group.description }}</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header"><h3>提醒检查动作</h3><span>直接触发后端规则检查</span></div>
|
<div class="panel-header"><h3>提醒检查动作</h3><span>直接触发后端规则检查</span></div>
|
||||||
<div class="action-row left">
|
<div class="action-row left">
|
||||||
@ -75,15 +74,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-header"><h3>前后端联动说明</h3><span>配置中心连接提醒、报表与权限规则</span></div>
|
|
||||||
<div class="group-grid">
|
|
||||||
<article class="group-card"><h4>提醒联动</h4><p>直接触发后端提醒检查,配合提醒中心查看结果。</p></article>
|
|
||||||
<article class="group-card"><h4>报表联动</h4><p>报表分类映射将影响核对口径和导出结果。</p></article>
|
|
||||||
<article class="group-card"><h4>权限联动</h4><p>脱敏与可见范围规则可在权限中心继续收口。</p></article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="validation-list">
|
<div class="validation-list">
|
||||||
<article v-for="item in validationItems" :key="item.name" class="validation-card">
|
<article v-for="item in validationItems" :key="item.name" class="validation-card">
|
||||||
<strong>{{ item.name }}</strong>
|
<strong>{{ item.name }}</strong>
|
||||||
@ -120,18 +110,13 @@ const configKeys = [
|
|||||||
|
|
||||||
/** 配置项列表数据(含值、启用状态、修改标记) */
|
/** 配置项列表数据(含值、启用状态、修改标记) */
|
||||||
const configs = ref([]);
|
const configs = ref([]);
|
||||||
|
/** 加载时的原始配置快照,用于撤销修改 */
|
||||||
|
const originalConfigs = ref([]);
|
||||||
/** 保存操作进行中标识 */
|
/** 保存操作进行中标识 */
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
/** 最近一次保存时间 */
|
/** 最近一次保存时间 */
|
||||||
const lastSavedAt = ref('');
|
const lastSavedAt = ref('');
|
||||||
|
|
||||||
/** 配置分组定义(提醒规则、权限规则、利润规则) */
|
|
||||||
const groups = [
|
|
||||||
{ title: '提醒规则', description: '物流超时、欠款提醒、长期未下单。' },
|
|
||||||
{ title: '权限规则', description: '字段可见性、手机号脱敏、角色视图隔离。' },
|
|
||||||
{ title: '利润规则', description: '利润阈值、提成核对、电商订单剔除。' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 自动校验项列表(已对齐/待收口) */
|
/** 自动校验项列表(已对齐/待收口) */
|
||||||
const validationItems = ref([
|
const validationItems = ref([
|
||||||
{ name: '字段映射检查表', detail: '客户、订单、司机、报表字段已完成主口径整理。', state: 'done', stateText: '已对齐' },
|
{ name: '字段映射检查表', detail: '客户、订单、司机、报表字段已完成主口径整理。', state: 'done', stateText: '已对齐' },
|
||||||
@ -165,22 +150,27 @@ async function reloadConfig() {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
configs.value = items;
|
configs.value = items;
|
||||||
|
originalConfigs.value = JSON.parse(JSON.stringify(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAll() {
|
async function saveAll() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
for (const item of configs.value) {
|
const dirtyItems = configs.value.filter(item => item.dirty);
|
||||||
if (!item.dirty) continue;
|
if (!dirtyItems.length) {
|
||||||
await updateConfigItem(item.key, {
|
toast.success('没有需要保存的修改。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Promise.all(dirtyItems.map(item =>
|
||||||
|
updateConfigItem(item.key, {
|
||||||
config_value: item.value,
|
config_value: item.value,
|
||||||
config_name: item.name,
|
config_name: item.name,
|
||||||
remark: item.remark,
|
remark: item.remark,
|
||||||
status: item.enabled ? 1 : 0,
|
status: item.enabled ? 1 : 0,
|
||||||
});
|
}).then(() => { item.dirty = false; })
|
||||||
item.dirty = false;
|
));
|
||||||
}
|
|
||||||
lastSavedAt.value = new Date().toLocaleString();
|
lastSavedAt.value = new Date().toLocaleString();
|
||||||
|
originalConfigs.value = JSON.parse(JSON.stringify(configs.value));
|
||||||
toast.success('配置已保存。');
|
toast.success('配置已保存。');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error?.message || '保存配置失败');
|
toast.error(error?.message || '保存配置失败');
|
||||||
@ -190,7 +180,7 @@ async function saveAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetDirty() {
|
function resetDirty() {
|
||||||
configs.value.forEach((item) => { item.dirty = false; });
|
configs.value = JSON.parse(JSON.stringify(originalConfigs.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runChecks(type) {
|
async function runChecks(type) {
|
||||||
@ -209,5 +199,5 @@ onMounted(reloadConfig);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page{padding:0;display:grid;gap:16px}.page-header{display:flex;justify-content:space-between;gap:12px;align-items:center}.subline{margin:0;color:#64748b}.toggle{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:600}.validation-banner{display:flex;justify-content:space-between;gap:12px;padding:16px;border:1px solid #dbe3ef;border-radius:18px;background:#eff6ff}.validation-banner p{margin:6px 0 0;color:#475569}.banner-metrics{display:grid;grid-template-columns:repeat(3,minmax(100px,1fr));gap:10px}.banner-metrics article{padding:10px;border-radius:12px;background:#fff;border:1px solid #dbe3ef}.banner-metrics span{display:block;color:#64748b;font-size:12px}.banner-metrics strong{font-size:20px}.summary-grid,.group-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}.summary-card,.panel,.config-card,.group-card,.validation-card{border:1px solid #dbe3ef;border-radius:18px;padding:16px;background:#fff}.summary-card span{display:block;color:#64748b;margin-bottom:8px}.summary-card strong{font-size:24px}.panel-header{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px}.config-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}.config-card{display:grid;gap:10px}.config-card-top{display:flex;justify-content:space-between;gap:12px;align-items:center}.config-card p,.group-card p,.validation-card p{margin:0;color:#475569;line-height:1.6}.input-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.input-grid label{display:grid;gap:6px}.input-grid span{color:#475569;font-size:13px;font-weight:600}.action-row{display:flex;justify-content:flex-end;gap:10px;margin-top:14px}.action-row.left{justify-content:flex-start;flex-wrap:wrap}.primary-btn,.ghost-btn,input{border-radius:12px;border:1px solid #cbd5e1;padding:10px 12px;background:#fff}.primary-btn{background:#2563eb;color:#fff;border-color:#2563eb}.ghost-btn{background:#f8fafc;color:#334155}.group-card h4{margin:0 0 8px}.validation-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.status-pill{display:inline-flex;margin-top:10px;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600}.status-pill.done{background:#dcfce7;color:#166534}.status-pill.todo{background:#fef3c7;color:#92400e}@media (max-width: 780px){.page-header,.validation-banner,.config-card-top{flex-direction:column;align-items:flex-start}.banner-metrics{grid-template-columns:1fr 1fr}.input-grid{grid-template-columns:1fr}}
|
.page{padding:0;display:grid;gap:16px}.page-header{display:flex;justify-content:space-between;gap:12px;align-items:center}.subline{margin:0;color:#64748b}.toggle{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:600}.validation-banner{display:flex;justify-content:space-between;gap:12px;padding:16px;border:1px solid #dbe3ef;border-radius:18px;background:#eff6ff}.validation-banner p{margin:6px 0 0;color:#475569}.banner-metrics{display:grid;grid-template-columns:repeat(3,minmax(100px,1fr));gap:10px}.banner-metrics article{padding:10px;border-radius:12px;background:#fff;border:1px solid #dbe3ef}.banner-metrics span{display:block;color:#64748b;font-size:12px}.banner-metrics strong{font-size:20px}.summary-grid,.group-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}.summary-card,.panel,.config-card,.group-card,.validation-card{border:1px solid #dbe3ef;border-radius:18px;padding:16px;background:#fff}.summary-card span{display:block;color:#64748b;margin-bottom:8px}.summary-card strong{font-size:24px}.panel-header{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px}.config-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}.config-card{display:grid;gap:10px}.config-card-top{display:flex;justify-content:space-between;gap:12px;align-items:center}.config-card p,.group-card p,.validation-card p{margin:0;color:#475569;line-height:1.6}.input-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.input-grid label{display:grid;gap:6px}.input-grid span{color:#475569;font-size:13px;font-weight:600}.action-row{display:flex;justify-content:flex-end;gap:10px;margin-top:14px}.action-row.left{justify-content:flex-start;flex-wrap:wrap}.primary-btn,.ghost-btn,input{border-radius:12px;border:1px solid #cbd5e1;padding:10px 12px;background:#fff}.primary-btn{background:#2563eb;color:#fff;border-color:#2563eb}.ghost-btn{background:#f8fafc;color:#334155}.group-card h4{margin:0 0 8px}.validation-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.status-pill{display:inline-flex;margin-top:10px;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:600}.status-pill.done{background:#dcfce7;color:#166534}.status-pill.todo{background:#fef3c7;color:#92400e}.switch-group{display:grid;gap:6px}.switch-label{color:#475569;font-size:13px;font-weight:600}.switch-options{display:flex;gap:8px}.switch-options label{padding:8px 14px;border-radius:10px;border:1px solid #cbd5e1;cursor:pointer;font-size:13px;transition:all .15s}.switch-options label.selected{background:#eff6ff;border-color:#2563eb;color:#1d4ed8;font-weight:600}.switch-options input[type="radio"]{display:none}.panel-note{margin:0 0 12px;color:#64748b;font-size:13px}textarea{border-radius:12px;border:1px solid #cbd5e1;padding:10px 12px;background:#fff;resize:vertical;font-family:monospace;font-size:13px;min-height:60px}@media (max-width: 780px){.page-header,.validation-banner,.config-card-top{flex-direction:column;align-items:flex-start}.banner-metrics{grid-template-columns:1fr 1fr}.input-grid{grid-template-columns:1fr}}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user