331 lines
18 KiB
HTML
331 lines
18 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}系统字典管理 - AI 视界{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="w-full h-full overflow-y-auto p-8 lg:p-12 custom-scrollbar transition-all duration-500" id="mainContainer">
|
||
<div class="max-w-6xl mx-auto space-y-8">
|
||
<!-- 头部 -->
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-4">
|
||
<div
|
||
class="w-12 h-12 bg-indigo-600 text-white rounded-2xl flex items-center justify-center shadow-lg transition-transform hover:rotate-6">
|
||
<i data-lucide="book-open" class="w-7 h-7"></i>
|
||
</div>
|
||
<div>
|
||
<h1 class="text-3xl font-black text-slate-900 tracking-tight" id="pageTitle">数据字典控制中心</h1>
|
||
<p class="text-slate-400 text-sm font-medium" id="pageSubTitle">全局业务参数与 AI 模型配置</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<button id="backBtn"
|
||
class="hidden px-5 py-3 rounded-xl border border-slate-200 text-slate-400 text-sm font-bold hover:bg-slate-50 transition-all flex items-center gap-2">
|
||
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
||
返回列表
|
||
</button>
|
||
<button id="addAliasBtn"
|
||
onclick="openModal({dict_type: 'dict_type_alias', label: '新类型别名', value: 'target_type_key', cost: 0, sort_order: 0, is_active: true})"
|
||
class="hidden px-5 py-3 rounded-xl border border-indigo-200 text-indigo-600 text-sm font-bold hover:bg-indigo-50 transition-all flex items-center gap-2">
|
||
<i data-lucide="type" class="w-4 h-4"></i>
|
||
管理别名
|
||
</button>
|
||
<button id="addBtn" onclick="openModal()"
|
||
class="hidden btn-primary px-6 py-3 rounded-xl text-sm font-bold shadow-lg flex items-center gap-2">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
新增项
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视图 1:字典类型列表 -->
|
||
<div id="typeListView"
|
||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||
<!-- 动态加载类型卡片 -->
|
||
</div>
|
||
|
||
<!-- 视图 2:详情表格 -->
|
||
<div id="detailView" class="hidden space-y-4 animate-in fade-in slide-in-from-right-4 duration-500">
|
||
<div class="bg-white rounded-[2.5rem] shadow-xl border border-slate-100 overflow-hidden">
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr class="bg-slate-50 border-b border-slate-100">
|
||
<th
|
||
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none">
|
||
内容/显示名称</th>
|
||
<th
|
||
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none">
|
||
存储值 (Value)</th>
|
||
<th
|
||
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none">
|
||
计费 (Cost)</th>
|
||
<th
|
||
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none">
|
||
状态</th>
|
||
<th
|
||
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none">
|
||
操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dictTableBody" class="text-sm font-medium">
|
||
<!-- 动态加载 -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑弹窗 -->
|
||
<div id="dictModal"
|
||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-md z-50 flex items-center justify-center hidden opacity-0 transition-opacity duration-300">
|
||
<div
|
||
class="bg-white w-full max-w-lg rounded-[2.5rem] shadow-2xl p-10 space-y-8 transform scale-95 transition-transform duration-300 border border-white/50">
|
||
<div class="flex items-center justify-between">
|
||
<h2 id="modalTitle" class="text-2xl font-black text-slate-900 tracking-tight">配置项详情</h2>
|
||
<button onclick="closeModal()"
|
||
class="w-8 h-8 rounded-full bg-slate-50 text-slate-400 flex items-center justify-center hover:bg-slate-100 transition-all">
|
||
<i data-lucide="x" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<form id="dictForm" class="space-y-6">
|
||
<input type="hidden" id="dictId">
|
||
<div class="space-y-2">
|
||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">所属类型</label>
|
||
<input type="text" id="dictType" readonly
|
||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none text-slate-400 text-sm font-bold cursor-not-allowed">
|
||
</div>
|
||
<div class="space-y-2">
|
||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">显示标签 (Label)</label>
|
||
<input type="text" id="dictLabel" required placeholder="如: 高清 4K"
|
||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||
</div>
|
||
<div class="space-y-2">
|
||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">实际取值 (Value)</label>
|
||
<textarea id="dictValue" required rows="2" placeholder="API 所需的参数值"
|
||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold resize-none"></textarea>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div class="space-y-2">
|
||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">消耗积分</label>
|
||
<input type="number" id="dictCost" value="0"
|
||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||
</div>
|
||
<div class="space-y-2">
|
||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">排序权重</label>
|
||
<input type="number" id="dictOrder" value="0"
|
||
class="w-full bg-slate-50 border border-slate-100 rounded-2xl p-4 outline-none focus:border-indigo-500 transition-all text-sm font-bold">
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center justify-between bg-slate-50 p-4 rounded-2xl border border-slate-100/50">
|
||
<div class="flex items-center gap-2">
|
||
<i data-lucide="eye" class="w-4 h-4 text-indigo-500"></i>
|
||
<span class="text-sm font-bold text-slate-600">前端可见状态</span>
|
||
</div>
|
||
<label class="relative inline-flex items-center cursor-pointer">
|
||
<input type="checkbox" id="dictActive" checked class="sr-only peer">
|
||
<div
|
||
class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600">
|
||
</div>
|
||
</label>
|
||
</div>
|
||
<button type="submit"
|
||
class="w-full btn-primary py-4 rounded-2xl font-bold shadow-xl shadow-indigo-100 hover:scale-[1.02] active:scale-[0.98] transition-all">保存配置变更</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
let currentCategory = null;
|
||
let categoriesList = [];
|
||
|
||
// 加载所有字典类型
|
||
async function loadCategories() {
|
||
const r = await fetch('/api/admin/dict_types');
|
||
const d = await r.json();
|
||
categoriesList = d.types;
|
||
|
||
const container = document.getElementById('typeListView');
|
||
container.innerHTML = d.types.map(cat => `
|
||
<div onclick="enterCategory('${cat.type}', '${cat.name}')" class="group bg-white p-8 rounded-[2.5rem] shadow-xl border border-slate-100 hover:border-indigo-500 transition-all cursor-pointer relative overflow-hidden">
|
||
<div class="absolute top-0 right-0 w-32 h-32 bg-indigo-50 rounded-full -mr-16 -mt-16 group-hover:scale-110 transition-transform duration-500"></div>
|
||
<div class="relative z-10 space-y-4">
|
||
<div class="w-12 h-12 bg-slate-900 text-white rounded-2xl flex items-center justify-center group-hover:bg-indigo-600 transition-colors shadow-lg">
|
||
<i data-lucide="${getIconForType(cat.type)}" class="w-6 h-6"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xl font-black text-slate-900 tracking-tight">${cat.name}</h3>
|
||
<p class="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-1">${cat.type}</p>
|
||
</div>
|
||
<div class="flex items-center justify-between pt-2">
|
||
<span class="px-3 py-1 bg-slate-50 text-slate-400 rounded-lg text-xs font-black uppercase tracking-tight">${cat.count} 个配置项</span>
|
||
<div class="w-8 h-8 rounded-full border border-slate-100 flex items-center justify-center text-slate-300 group-hover:text-indigo-600 group-hover:border-indigo-100 transition-all">
|
||
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
lucide.createIcons();
|
||
}
|
||
|
||
function getIconForType(type) {
|
||
const map = {
|
||
'ai_model': 'cpu',
|
||
'aspect_ratio': 'maximize',
|
||
'ai_image_size': 'layout',
|
||
'prompt_tpl': 'file-text',
|
||
'video_model': 'video',
|
||
'video_prompt': 'clapperboard'
|
||
};
|
||
return map[type] || 'settings-2';
|
||
}
|
||
|
||
// 进入某个分类的详情视图
|
||
async function enterCategory(type, name) {
|
||
currentCategory = { type, name };
|
||
|
||
// 更新 UI 状态
|
||
document.getElementById('typeListView').classList.add('hidden');
|
||
document.getElementById('detailView').classList.remove('hidden');
|
||
document.getElementById('backBtn').classList.remove('hidden');
|
||
document.getElementById('addBtn').classList.remove('hidden');
|
||
document.getElementById('addAliasBtn').classList.add('hidden'); // 详情页隐藏别名按钮
|
||
|
||
document.getElementById('pageTitle').innerText = name;
|
||
document.getElementById('pageSubTitle').innerText = `正在管理 ${type} 类型的详细参数`;
|
||
|
||
await loadDetails(type);
|
||
}
|
||
|
||
// 返回列表视图
|
||
function exitCategory() {
|
||
currentCategory = null;
|
||
document.getElementById('detailView').classList.add('hidden');
|
||
document.getElementById('typeListView').classList.remove('hidden');
|
||
document.getElementById('backBtn').classList.add('hidden');
|
||
document.getElementById('addBtn').classList.add('hidden');
|
||
document.getElementById('addAliasBtn').classList.remove('hidden'); // 在主列表显示别名管理入口
|
||
|
||
document.getElementById('pageTitle').innerText = '数据字典控制中心';
|
||
document.getElementById('pageSubTitle').innerText = '全局业务参数与 AI 模型配置';
|
||
|
||
loadCategories(); // 刷新计数
|
||
}
|
||
|
||
document.getElementById('backBtn').onclick = exitCategory;
|
||
|
||
// 获取分类详情
|
||
async function loadDetails(type) {
|
||
const r = await fetch(`/api/admin/dicts?type=${type}`);
|
||
const d = await r.json();
|
||
const body = document.getElementById('dictTableBody');
|
||
|
||
body.innerHTML = d.dicts.map(item => `
|
||
<tr class="border-b border-slate-50 hover:bg-slate-50 transition-colors">
|
||
<td class="px-8 py-5">
|
||
<div class="text-slate-900 font-bold">${item.label}</div>
|
||
<div class="text-[9px] text-slate-300 font-black uppercase tracking-tighter">ID: ${item.id} · 排序: ${item.sort_order}</div>
|
||
</td>
|
||
<td class="px-8 py-5">
|
||
<div class="text-slate-500 text-xs font-medium max-w-sm truncate" title="${item.value}">${item.value}</div>
|
||
</td>
|
||
<td class="px-8 py-5">
|
||
<span class="px-2.5 py-1 bg-amber-50 text-amber-600 rounded-lg text-xs font-black">${item.cost} <small class="text-[8px]">POINTS</small></span>
|
||
</td>
|
||
<td class="px-8 py-5">
|
||
<div class="flex items-center gap-2 ${item.is_active ? 'text-emerald-500' : 'text-slate-300'}">
|
||
<div class="w-2 h-2 rounded-full ${item.is_active ? 'bg-emerald-500 animate-pulse' : 'bg-slate-300'}"></div>
|
||
<span class="text-[10px] font-black uppercase">${item.is_active ? 'Active' : 'Disabled'}</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-8 py-5">
|
||
<div class="flex gap-2">
|
||
<button onclick='editDict(${JSON.stringify(item)})' class="p-2 text-indigo-400 hover:bg-indigo-50 rounded-xl transition-all">
|
||
<i data-lucide="settings-2" class="w-4 h-4"></i>
|
||
</button>
|
||
<button onclick="deleteDict(${item.id})" class="p-2 text-rose-300 hover:bg-rose-50 rounded-xl transition-all">
|
||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
lucide.createIcons();
|
||
}
|
||
|
||
function openModal(item = null) {
|
||
const modal = document.getElementById('dictModal');
|
||
document.getElementById('modalTitle').innerText = item ? '编辑配置项' : '新增配置项';
|
||
document.getElementById('dictId').value = item ? item.id : '';
|
||
document.getElementById('dictType').value = currentCategory ? currentCategory.type : '';
|
||
document.getElementById('dictLabel').value = item ? item.label : '';
|
||
document.getElementById('dictValue').value = item ? item.value : '';
|
||
document.getElementById('dictCost').value = item ? item.cost : 0;
|
||
document.getElementById('dictOrder').value = item ? item.sort_order : 0;
|
||
document.getElementById('dictActive').checked = item ? item.is_active : true;
|
||
|
||
modal.classList.remove('hidden');
|
||
setTimeout(() => {
|
||
modal.classList.remove('opacity-0');
|
||
modal.querySelector('div').classList.remove('scale-95');
|
||
}, 10);
|
||
}
|
||
|
||
function closeModal() {
|
||
const modal = document.getElementById('dictModal');
|
||
modal.classList.add('opacity-0');
|
||
modal.querySelector('div').classList.add('scale-95');
|
||
setTimeout(() => modal.classList.add('hidden'), 300);
|
||
}
|
||
|
||
window.editDict = (item) => openModal(item);
|
||
|
||
window.deleteDict = async (id) => {
|
||
if (!confirm('确定永久删除该配置项?此操作不可逆,可能影响线上渲染。')) return;
|
||
const r = await fetch('/api/admin/dicts/delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id })
|
||
});
|
||
const d = await r.json();
|
||
if (d.message) {
|
||
showToast(d.message, 'success');
|
||
loadDetails(currentCategory.type);
|
||
}
|
||
};
|
||
|
||
document.getElementById('dictForm').onsubmit = async (e) => {
|
||
e.preventDefault();
|
||
const payload = {
|
||
id: document.getElementById('dictId').value,
|
||
dict_type: document.getElementById('dictType').value,
|
||
label: document.getElementById('dictLabel').value,
|
||
value: document.getElementById('dictValue').value,
|
||
cost: parseInt(document.getElementById('dictCost').value),
|
||
sort_order: parseInt(document.getElementById('dictOrder').value),
|
||
is_active: document.getElementById('dictActive').checked
|
||
};
|
||
|
||
const r = await fetch('/api/admin/dicts', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const d = await r.json();
|
||
if (d.message) {
|
||
showToast(d.message, 'success');
|
||
closeModal();
|
||
loadDetails(currentCategory.type);
|
||
}
|
||
};
|
||
|
||
loadCategories();
|
||
// 默认在列表页显示别名按钮
|
||
document.getElementById('addAliasBtn').classList.remove('hidden');
|
||
</script>
|
||
{% endblock %} |