admin update
This commit is contained in:
1100
static/admin.html
1100
static/admin.html
File diff suppressed because it is too large
Load Diff
579
static/admin.html.bak
Normal file
579
static/admin.html.bak
Normal file
@@ -0,0 +1,579 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SAM3 项目管理后台</title>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
[v-cloak] { display: none; }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.5s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen text-gray-800">
|
||||
<div id="app" v-cloak>
|
||||
<!-- 登录页 -->
|
||||
<div v-if="!isLoggedIn" class="flex items-center justify-center min-h-screen">
|
||||
<div class="bg-white p-8 rounded-lg shadow-lg w-96">
|
||||
<h1 class="text-2xl font-bold mb-6 text-center text-blue-600">SAM3 管理后台</h1>
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2">管理员密码</label>
|
||||
<input v-model="password" type="password" @keyup.enter="login" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="请输入密码">
|
||||
</div>
|
||||
<button @click="login" class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300">
|
||||
登录
|
||||
</button>
|
||||
<p v-if="loginError" class="text-red-500 text-xs italic mt-2">{{ loginError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主界面 -->
|
||||
<div v-else class="flex h-screen overflow-hidden">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="w-64 bg-slate-800 text-white flex flex-col">
|
||||
<div class="p-6 border-b border-slate-700">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2">
|
||||
<i class="fas fa-layer-group"></i> SAM3 Admin
|
||||
</h2>
|
||||
</div>
|
||||
<nav class="flex-1 p-4 space-y-2">
|
||||
<a href="#" @click.prevent="currentTab = 'dashboard'" :class="{'bg-blue-600': currentTab === 'dashboard', 'hover:bg-slate-700': currentTab !== 'dashboard'}" class="block py-2.5 px-4 rounded transition duration-200">
|
||||
<i class="fas fa-chart-line w-6"></i> 识别记录
|
||||
</a>
|
||||
<a href="#" @click.prevent="currentTab = 'files'" :class="{'bg-blue-600': currentTab === 'files', 'hover:bg-slate-700': currentTab !== 'files'}" class="block py-2.5 px-4 rounded transition duration-200">
|
||||
<i class="fas fa-folder-open w-6"></i> 文件管理
|
||||
</a>
|
||||
<a href="#" @click.prevent="currentTab = 'prompts'" :class="{'bg-blue-600': currentTab === 'prompts', 'hover:bg-slate-700': currentTab !== 'prompts'}" class="block py-2.5 px-4 rounded transition duration-200">
|
||||
<i class="fas fa-comment-dots w-6"></i> 提示词管理
|
||||
</a>
|
||||
<a href="#" @click.prevent="currentTab = 'settings'" :class="{'bg-blue-600': currentTab === 'settings', 'hover:bg-slate-700': currentTab !== 'settings'}" class="block py-2.5 px-4 rounded transition duration-200">
|
||||
<i class="fas fa-cogs w-6"></i> 系统设置
|
||||
</a>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-slate-700">
|
||||
<button @click="logout" class="w-full bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition duration-200">
|
||||
<i class="fas fa-sign-out-alt"></i> 退出登录
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1 overflow-y-auto bg-gray-50 p-8">
|
||||
<!-- 识别记录 Dashboard -->
|
||||
<div v-if="currentTab === 'dashboard'">
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2 flex items-center justify-between">
|
||||
<span>最近识别记录</span>
|
||||
<button @click="fetchHistory" class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded text-sm transition shadow-sm">
|
||||
<i class="fas fa-sync-alt mr-1"></i> 刷新
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden border border-gray-100">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full leading-normal">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-5 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider w-32">时间</th>
|
||||
<th class="px-5 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider w-24">类型</th>
|
||||
<th class="px-5 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Prompt / 详情</th>
|
||||
<th class="px-5 py-3 border-b border-gray-200 bg-gray-50 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider w-24">耗时</th>
|
||||
<th class="px-5 py-3 border-b border-gray-200 bg-gray-50 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider w-24">状态</th>
|
||||
<th class="px-5 py-3 border-b border-gray-200 bg-gray-50 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider w-20">查看</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(record, index) in history" :key="index" class="hover:bg-gray-50 transition duration-150">
|
||||
<td class="px-5 py-4 border-b border-gray-100 bg-white text-sm text-gray-600 whitespace-nowrap">
|
||||
<div class="font-medium">{{ formatDate(record.timestamp).split(' ')[0] }}</div>
|
||||
<div class="text-xs text-gray-400">{{ formatDate(record.timestamp).split(' ')[1] }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 border-b border-gray-100 bg-white text-sm">
|
||||
<span :class="getTypeBadgeClass(record.type)" class="px-2 py-1 text-xs font-semibold rounded-md shadow-sm">
|
||||
{{ record.type }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 border-b border-gray-100 bg-white text-sm">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- Prompt -->
|
||||
<div v-if="record.prompt" class="font-medium text-gray-800 break-words flex items-center gap-2">
|
||||
<i class="fas fa-keyboard text-gray-300 text-xs"></i>
|
||||
{{ record.prompt }}
|
||||
</div>
|
||||
|
||||
<!-- Translated Prompt -->
|
||||
<div v-if="record.final_prompt && record.final_prompt !== record.prompt" class="text-xs text-gray-500 flex items-center gap-2">
|
||||
<i class="fas fa-language text-blue-300 text-xs"></i>
|
||||
<span class="italic bg-gray-50 px-1 rounded">{{ record.final_prompt }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div class="text-xs text-gray-400 mt-1 flex items-center gap-2">
|
||||
<i class="fas fa-info-circle text-gray-300"></i>
|
||||
{{ record.details }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 border-b border-gray-100 bg-white text-sm text-center">
|
||||
<div v-if="record.duration" :class="getDurationClass(record.duration)" class="font-mono text-xs inline-block px-2 py-0.5 rounded">
|
||||
{{ record.duration.toFixed(2) }}s
|
||||
</div>
|
||||
<div v-else class="text-gray-300 text-xs">-</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 border-b border-gray-100 bg-white text-sm text-center">
|
||||
<span :class="record.status === 'success' ? 'text-green-700 bg-green-100 ring-1 ring-green-200' : (record.status === 'partial_success' ? 'text-yellow-700 bg-yellow-100 ring-1 ring-yellow-200' : 'text-red-700 bg-red-100 ring-1 ring-red-200')" class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full">
|
||||
<i :class="record.status === 'success' ? 'fas fa-check-circle' : (record.status === 'partial_success' ? 'fas fa-exclamation-circle' : 'fas fa-times-circle')" class="mr-1 mt-0.5"></i>
|
||||
{{ record.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 border-b border-gray-100 bg-white text-sm text-center">
|
||||
<button v-if="record.result_path" @click="viewResult(record.result_path)" class="text-blue-500 hover:text-blue-700 transition transform hover:scale-110" title="查看结果">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</button>
|
||||
<span v-else class="text-gray-300 cursor-not-allowed">
|
||||
<i class="fas fa-eye-slash"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="history.length === 0">
|
||||
<td colspan="6" class="px-5 py-10 border-b border-gray-200 bg-white text-sm text-center text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<i class="fas fa-inbox text-4xl mb-3 text-gray-200"></i>
|
||||
暂无记录
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件管理 Files -->
|
||||
<div v-if="currentTab === 'files'">
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">文件资源管理 (static/results)</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-bold">当前路径:</span> /static/results/{{ currentPath }}
|
||||
<button v-if="currentPath" @click="navigateUp" class="ml-2 text-blue-500 hover:underline text-xs">
|
||||
<i class="fas fa-level-up-alt"></i> 返回上一级
|
||||
</button>
|
||||
</div>
|
||||
<button @click="fetchFiles" class="text-blue-500 hover:text-blue-700">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
<div v-for="file in files" :key="file.name" class="border rounded-lg p-2 hover:shadow-md transition cursor-pointer relative group">
|
||||
<!-- Folder -->
|
||||
<div v-if="file.is_dir" @click="enterDir(file.name)" class="flex flex-col items-center justify-center h-32">
|
||||
<i class="fas fa-folder text-yellow-400 text-4xl mb-2"></i>
|
||||
<span class="text-xs text-center break-all px-1">{{ file.name }}</span>
|
||||
<span class="text-xs text-gray-400">{{ file.count }} 项</span>
|
||||
</div>
|
||||
<!-- Image File -->
|
||||
<div v-else-if="isImage(file.name)" @click="previewImage(file.url)" class="flex flex-col items-center justify-center h-32">
|
||||
<img :src="file.url" class="h-20 w-auto object-contain mb-2 rounded" loading="lazy">
|
||||
<span class="text-xs text-center break-all px-1 truncate w-full">{{ file.name }}</span>
|
||||
</div>
|
||||
<!-- Other File -->
|
||||
<div v-else class="flex flex-col items-center justify-center h-32">
|
||||
<i class="fas fa-file text-gray-400 text-4xl mb-2"></i>
|
||||
<span class="text-xs text-center break-all px-1">{{ file.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Delete Button (Hover) -->
|
||||
<button @click.stop="deleteFile(file.name)" class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition hover:bg-red-700" title="删除">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="files.length === 0" class="text-center py-10 text-gray-500">
|
||||
此目录下没有文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词管理 Prompts -->
|
||||
<div v-if="currentTab === 'prompts'">
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">提示词管理</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div v-for="(content, key) in prompts" :key="key" class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-700 flex items-center gap-2">
|
||||
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded border border-blue-400 font-mono">{{ key }}</span>
|
||||
<span class="text-sm font-normal text-gray-500">{{ getPromptDescription(key) }}</span>
|
||||
</h3>
|
||||
<button @click="savePrompt(key)" class="bg-green-500 hover:bg-green-600 text-white font-bold py-1 px-3 rounded text-sm transition">
|
||||
<i class="fas fa-save mr-1"></i> 保存
|
||||
</button>
|
||||
</div>
|
||||
<textarea v-model="prompts[key]" rows="6" class="w-full p-3 border rounded font-mono text-sm bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置 Settings -->
|
||||
<div v-if="currentTab === 'settings'">
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">系统设置</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold mb-4 text-gray-700">自动清理配置</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center border-b pb-2">
|
||||
<span class="text-gray-600">启用自动清理</span>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="cleanupConfig.enabled" class="sr-only peer">
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[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-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="border-b pb-2">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-gray-600">文件保留时长 (秒)</span>
|
||||
<span class="text-xs text-gray-500">{{ (cleanupConfig.lifetime / 3600).toFixed(1) }} 小时</span>
|
||||
</div>
|
||||
<input type="number" v-model.number="cleanupConfig.lifetime" class="w-full border rounded px-2 py-1 text-sm">
|
||||
</div>
|
||||
|
||||
<div class="border-b pb-2">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-gray-600">检查间隔 (秒)</span>
|
||||
<span class="text-xs text-gray-500">{{ (cleanupConfig.interval / 60).toFixed(1) }} 分钟</span>
|
||||
</div>
|
||||
<input type="number" v-model.number="cleanupConfig.interval" class="w-full border rounded px-2 py-1 text-sm">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button @click="saveCleanupConfig" class="flex-1 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition text-sm">
|
||||
<i class="fas fa-save mr-2"></i> 保存配置
|
||||
</button>
|
||||
<button @click="triggerCleanup" :disabled="cleaning" class="flex-1 bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded transition text-sm">
|
||||
<i class="fas fa-broom mr-2"></i> {{ cleaning ? '清理中...' : '立即清理' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-bold mb-4 text-gray-700">系统信息</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between border-b pb-2">
|
||||
<span class="text-gray-600">模型</span>
|
||||
<span class="font-mono">SAM3</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center border-b pb-2">
|
||||
<span class="text-gray-600">多模态模型</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<select v-model="currentModel" @change="updateModel" class="border rounded px-2 py-1 text-sm font-mono bg-white">
|
||||
<option v-for="model in availableModels" :key="model" :value="model">
|
||||
{{ model }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between border-b pb-2">
|
||||
<span class="text-gray-600">设备</span>
|
||||
<span class="font-mono">{{ deviceInfo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览模态框 -->
|
||||
<div v-if="previewUrl" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-90" @click="previewUrl = null">
|
||||
<div class="relative max-w-4xl max-h-screen p-4">
|
||||
<img :src="previewUrl" class="max-h-[90vh] max-w-full rounded shadow-lg" @click.stop>
|
||||
<button class="absolute top-0 right-0 m-4 text-white text-3xl font-bold hover:text-gray-300" @click="previewUrl = null">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref, onMounted, computed } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const isLoggedIn = ref(false);
|
||||
const password = ref('');
|
||||
const loginError = ref('');
|
||||
const currentTab = ref('dashboard');
|
||||
const history = ref([]);
|
||||
const files = ref([]);
|
||||
const currentPath = ref('');
|
||||
const previewUrl = ref(null);
|
||||
const cleaning = ref(false);
|
||||
const deviceInfo = ref('Loading...');
|
||||
const currentModel = ref('');
|
||||
const availableModels = ref([]);
|
||||
const cleanupConfig = ref({
|
||||
enabled: true,
|
||||
lifetime: 3600,
|
||||
interval: 600
|
||||
});
|
||||
const prompts = ref({});
|
||||
|
||||
// 检查登录状态
|
||||
const checkLogin = () => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
if (token) {
|
||||
isLoggedIn.value = true;
|
||||
fetchHistory();
|
||||
fetchSystemInfo();
|
||||
}
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('password', password.value);
|
||||
const res = await axios.post('/admin/login', formData);
|
||||
if (res.data.status === 'success') {
|
||||
localStorage.setItem('admin_token', 'logged_in'); // 简单标记,实际由Cookie控制
|
||||
isLoggedIn.value = true;
|
||||
loginError.value = '';
|
||||
fetchHistory();
|
||||
fetchSystemInfo();
|
||||
fetchPrompts();
|
||||
}
|
||||
} catch (e) {
|
||||
loginError.value = '密码错误';
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('admin_token');
|
||||
document.cookie = "admin_session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
isLoggedIn.value = false;
|
||||
password.value = '';
|
||||
};
|
||||
|
||||
const fetchHistory = async () => {
|
||||
try {
|
||||
const res = await axios.get('/admin/api/history');
|
||||
history.value = res.data.reverse(); // 最新在前
|
||||
} catch (e) {
|
||||
if (e.response && e.response.status === 401) logout();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const res = await axios.get(`/admin/api/files?path=${currentPath.value}`);
|
||||
files.value = res.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSystemInfo = async () => {
|
||||
try {
|
||||
const res = await axios.get('/admin/api/config');
|
||||
deviceInfo.value = res.data.device;
|
||||
currentModel.value = res.data.current_qwen_model;
|
||||
availableModels.value = res.data.available_qwen_models;
|
||||
if (res.data.cleanup_config) {
|
||||
cleanupConfig.value = res.data.cleanup_config;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const saveCleanupConfig = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('enabled', cleanupConfig.value.enabled);
|
||||
formData.append('lifetime', cleanupConfig.value.lifetime);
|
||||
formData.append('interval', cleanupConfig.value.interval);
|
||||
|
||||
const res = await axios.post('/admin/api/config/cleanup', formData);
|
||||
alert(res.data.message);
|
||||
} catch (e) {
|
||||
alert('保存配置失败: ' + (e.response?.data?.detail || e.message));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
try {
|
||||
const res = await axios.get('/admin/api/prompts');
|
||||
prompts.value = res.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const savePrompt = async (key) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('key', key);
|
||||
formData.append('content', prompts.value[key]);
|
||||
const res = await axios.post('/admin/api/prompts', formData);
|
||||
alert(res.data.message);
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + (e.response?.data?.detail || e.message));
|
||||
}
|
||||
};
|
||||
|
||||
const getPromptDescription = (key) => {
|
||||
const map = {
|
||||
'translate': 'Prompt 翻译 (中文 -> 英文)',
|
||||
'tarot_card_dual': '塔罗牌识别 (正/逆位对比模式)',
|
||||
'tarot_card_single': '塔罗牌识别 (单图模式)',
|
||||
'tarot_spread': '塔罗牌阵识别',
|
||||
'face_analysis': '人脸/属性分析 (Qwen-VL)'
|
||||
};
|
||||
return map[key] || '';
|
||||
};
|
||||
|
||||
const updateModel = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('model', currentModel.value);
|
||||
const res = await axios.post('/admin/api/config/model', formData);
|
||||
alert(res.data.message);
|
||||
} catch (e) {
|
||||
alert('更新模型失败');
|
||||
console.error(e);
|
||||
// Revert on failure
|
||||
fetchSystemInfo();
|
||||
}
|
||||
};
|
||||
|
||||
const enterDir = (dirName) => {
|
||||
currentPath.value = currentPath.value ? `${currentPath.value}/${dirName}` : dirName;
|
||||
fetchFiles();
|
||||
};
|
||||
|
||||
const navigateUp = () => {
|
||||
if (!currentPath.value) return;
|
||||
const parts = currentPath.value.split('/');
|
||||
parts.pop();
|
||||
currentPath.value = parts.join('/');
|
||||
fetchFiles();
|
||||
};
|
||||
|
||||
const deleteFile = async (name) => {
|
||||
if (!confirm(`确定要删除 ${name} 吗?`)) return;
|
||||
try {
|
||||
const fullPath = currentPath.value ? `${currentPath.value}/${name}` : name;
|
||||
await axios.delete(`/admin/api/files/${fullPath}`);
|
||||
fetchFiles();
|
||||
} catch (e) {
|
||||
alert('删除失败: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerCleanup = async () => {
|
||||
cleaning.value = true;
|
||||
try {
|
||||
const res = await axios.post('/admin/api/cleanup');
|
||||
alert(`清理完成: ${res.data.message}`);
|
||||
fetchFiles(); // 刷新文件列表
|
||||
} catch (e) {
|
||||
alert('清理失败');
|
||||
} finally {
|
||||
cleaning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const viewResult = (path) => {
|
||||
// path format: "results/subdir/file.jpg" or "results/file.jpg"
|
||||
currentTab.value = 'files';
|
||||
|
||||
// Remove "results/" prefix
|
||||
// Note: path usually comes from backend as "results/..."
|
||||
let relativePath = path;
|
||||
if (relativePath.startsWith('results/')) {
|
||||
relativePath = relativePath.substring(8); // Remove "results/"
|
||||
}
|
||||
|
||||
// Check if it looks like a file (has extension)
|
||||
const isFile = /\.[a-zA-Z0-9]+$/.test(relativePath);
|
||||
|
||||
if (isFile) {
|
||||
// It's a file
|
||||
const lastSlashIndex = relativePath.lastIndexOf('/');
|
||||
let dirPath = '';
|
||||
|
||||
if (lastSlashIndex !== -1) {
|
||||
dirPath = relativePath.substring(0, lastSlashIndex);
|
||||
}
|
||||
|
||||
currentPath.value = dirPath;
|
||||
fetchFiles();
|
||||
|
||||
// Show preview immediately
|
||||
previewUrl.value = '/static/' + path;
|
||||
} else {
|
||||
// It's likely a directory
|
||||
currentPath.value = relativePath;
|
||||
fetchFiles();
|
||||
}
|
||||
};
|
||||
|
||||
const previewImage = (path) => {
|
||||
previewUrl.value = path;
|
||||
};
|
||||
|
||||
const isImage = (name) => {
|
||||
return /\.(jpg|jpeg|png|gif|webp)$/i.test(name);
|
||||
};
|
||||
|
||||
const formatDate = (ts) => {
|
||||
return new Date(ts * 1000).toLocaleString();
|
||||
};
|
||||
|
||||
const getDurationClass = (duration) => {
|
||||
if (duration < 2.0) return 'text-green-600 bg-green-50';
|
||||
if (duration < 5.0) return 'text-yellow-600 bg-yellow-50';
|
||||
return 'text-red-600 bg-red-50';
|
||||
};
|
||||
|
||||
const getTypeBadgeClass = (type) => {
|
||||
const map = {
|
||||
'general': 'bg-blue-50 text-blue-600 border border-blue-100',
|
||||
'tarot': 'bg-purple-50 text-purple-600 border border-purple-100',
|
||||
'tarot-recognize': 'bg-indigo-50 text-indigo-600 border border-indigo-100',
|
||||
'face': 'bg-pink-50 text-pink-600 border border-pink-100'
|
||||
};
|
||||
return map[type] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
// Watch tab change to fetch data
|
||||
Vue.watch(currentTab, (newTab) => {
|
||||
if (newTab === 'files') fetchFiles();
|
||||
if (newTab === 'dashboard') fetchHistory();
|
||||
if (newTab === 'prompts') fetchPrompts();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
checkLogin();
|
||||
});
|
||||
|
||||
return {
|
||||
isLoggedIn, password, loginError, login, logout,
|
||||
currentTab, history, files, currentPath,
|
||||
enterDir, navigateUp, deleteFile, triggerCleanup,
|
||||
viewResult, previewImage, isImage, previewUrl,
|
||||
formatDate, getDurationClass, getTypeBadgeClass, cleaning, deviceInfo,
|
||||
currentModel, availableModels, updateModel,
|
||||
cleanupConfig, saveCleanupConfig,
|
||||
prompts, fetchPrompts, savePrompt, getPromptDescription
|
||||
};
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/results/seg_72b3c186467d48bf8591c9699ce90ca7.jpg
Normal file
BIN
static/results/seg_72b3c186467d48bf8591c9699ce90ca7.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Reference in New Issue
Block a user