2026-01-12 00:53:31 +08:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}系统字典管理 - AI 视界{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
2026-01-12 23:29:29 +08:00
|
|
|
<div class="w-full h-full overflow-y-auto p-8 lg:p-12 custom-scrollbar">
|
2026-01-12 00:53:31 +08:00
|
|
|
<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">
|
|
|
|
|
<i data-lucide="book-open" class="w-7 h-7"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统字典管理</h1>
|
|
|
|
|
<p class="text-slate-400 text-sm">统一维护模型、比例、提示词等系统参数</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button onclick="openModal()" class="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 class="flex gap-4 bg-white p-4 rounded-3xl shadow-sm border border-slate-100">
|
|
|
|
|
<button onclick="loadDicts('')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all bg-slate-100 text-slate-600 active-filter">全部</button>
|
|
|
|
|
<button onclick="loadDicts('ai_model')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">AI 模型</button>
|
|
|
|
|
<button onclick="loadDicts('aspect_ratio')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">画面比例</button>
|
|
|
|
|
<button onclick="loadDicts('ai_image_size')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">输出尺寸</button>
|
|
|
|
|
<button onclick="loadDicts('prompt_tpl')" class="dict-filter-btn px-4 py-2 rounded-xl text-xs font-bold transition-all hover:bg-slate-50 text-slate-400">提示词模板</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 表格 -->
|
|
|
|
|
<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">类型</th>
|
|
|
|
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">显示名称</th>
|
|
|
|
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">存储值/内容</th>
|
|
|
|
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">积分消耗</th>
|
|
|
|
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">状态</th>
|
|
|
|
|
<th class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest">操作</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="dictTableBody" class="text-sm font-medium">
|
|
|
|
|
<!-- 动态加载 -->
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 编辑弹窗 -->
|
|
|
|
|
<div id="dictModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm 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">
|
|
|
|
|
<h2 id="modalTitle" class="text-2xl font-black text-slate-900">新增字典项</h2>
|
|
|
|
|
<form id="dictForm" class="space-y-5">
|
|
|
|
|
<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>
|
|
|
|
|
<select id="dictType" 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">
|
|
|
|
|
<option value="ai_model">AI 模型 (ai_model)</option>
|
|
|
|
|
<option value="aspect_ratio">画面比例 (aspect_ratio)</option>
|
|
|
|
|
<option value="ai_image_size">输出尺寸 (ai_image_size)</option>
|
|
|
|
|
<option value="prompt_tpl">提示词模板 (prompt_tpl)</option>
|
|
|
|
|
</select>
|
|
|
|
|
</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 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="3" 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 gap-2 pt-2">
|
|
|
|
|
<input type="checkbox" id="dictActive" checked class="w-4 h-4 rounded border-slate-200 text-indigo-600 focus:ring-indigo-500">
|
|
|
|
|
<label for="dictActive" class="text-sm font-bold text-slate-600">立即启用</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-4 pt-4">
|
|
|
|
|
<button type="button" onclick="closeModal()" class="flex-1 px-8 py-4 rounded-2xl border border-slate-100 font-bold text-slate-400 hover:bg-slate-50 transition-all">取消</button>
|
|
|
|
|
<button type="submit" class="flex-1 btn-primary px-8 py-4 rounded-2xl font-bold shadow-lg shadow-indigo-100">保存配置</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block scripts %}
|
|
|
|
|
<script>
|
|
|
|
|
let currentType = '';
|
|
|
|
|
|
|
|
|
|
async function loadDicts(type = '') {
|
|
|
|
|
currentType = type;
|
|
|
|
|
// 更新按钮样式
|
|
|
|
|
document.querySelectorAll('.dict-filter-btn').forEach(btn => {
|
|
|
|
|
const btnType = btn.getAttribute('onclick').match(/'(.*)'/)[1];
|
|
|
|
|
if(btnType === type) {
|
|
|
|
|
btn.classList.add('bg-slate-100', 'text-slate-600');
|
|
|
|
|
btn.classList.remove('hover:bg-slate-50', 'text-slate-400');
|
|
|
|
|
} else {
|
|
|
|
|
btn.classList.remove('bg-slate-100', 'text-slate-600');
|
|
|
|
|
btn.classList.add('hover:bg-slate-50', 'text-slate-400');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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/50 transition-colors">
|
|
|
|
|
<td class="px-8 py-5">
|
|
|
|
|
<span class="px-2 py-0.5 rounded text-[10px] font-black uppercase ${
|
|
|
|
|
item.dict_type === 'ai_model' ? 'bg-indigo-50 text-indigo-600' :
|
|
|
|
|
item.dict_type === 'aspect_ratio' ? 'bg-emerald-50 text-emerald-600' :
|
|
|
|
|
item.dict_type === 'ai_image_size' ? 'bg-rose-50 text-rose-600' : 'bg-amber-50 text-amber-600'
|
|
|
|
|
}">${item.dict_type}</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-8 py-5 text-slate-700 font-bold">${item.label}</td>
|
|
|
|
|
<td class="px-8 py-5 text-slate-400 text-xs truncate max-w-xs" title="${item.value}">${item.value}</td>
|
|
|
|
|
<td class="px-8 py-5">
|
|
|
|
|
<span class="text-amber-600 font-black">${item.cost}</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-8 py-5">
|
|
|
|
|
<span class="${item.is_active ? 'text-emerald-500' : 'text-slate-300'}">
|
|
|
|
|
<i data-lucide="${item.is_active ? 'check-circle-2' : 'circle'}" class="w-4 h-4"></i>
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="px-8 py-5">
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<button onclick='editDict(${JSON.stringify(item)})' class="text-indigo-400 hover:text-indigo-600 transition-colors">
|
|
|
|
|
<i data-lucide="edit-3" class="w-4 h-4"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick="deleteDict(${item.id})" class="text-rose-300 hover:text-rose-500 transition-colors">
|
|
|
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
|
|
|
|
lucide.createIcons();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openModal(data = null) {
|
|
|
|
|
const modal = document.getElementById('dictModal');
|
|
|
|
|
const form = document.getElementById('dictForm');
|
|
|
|
|
document.getElementById('modalTitle').innerText = data ? '编辑字典项' : '新增字典项';
|
|
|
|
|
document.getElementById('dictId').value = data ? data.id : '';
|
|
|
|
|
document.getElementById('dictType').value = data ? data.dict_type : 'ai_model';
|
|
|
|
|
document.getElementById('dictLabel').value = data ? data.label : '';
|
|
|
|
|
document.getElementById('dictValue').value = data ? data.value : '';
|
|
|
|
|
document.getElementById('dictCost').value = data ? data.cost : 0;
|
|
|
|
|
document.getElementById('dictOrder').value = data ? data.sort_order : 0;
|
|
|
|
|
document.getElementById('dictActive').checked = data ? data.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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editDict(item) {
|
|
|
|
|
openModal(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteDict(id) {
|
|
|
|
|
if(!confirm('确定要删除此字典项吗?')) return;
|
|
|
|
|
try {
|
|
|
|
|
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');
|
|
|
|
|
loadDicts(currentType);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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();
|
|
|
|
|
loadDicts(currentType);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadDicts();
|
|
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|