ai_v/templates/dicts.html

331 lines
18 KiB
HTML
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.

{% 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 %}