521 lines
29 KiB
HTML
521 lines
29 KiB
HTML
<!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">最近识别记录</h2>
|
||
|
||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full leading-normal">
|
||
<thead>
|
||
<tr>
|
||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">时间</th>
|
||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">类型</th>
|
||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Prompt / 详情</th>
|
||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">状态</th>
|
||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(record, index) in history" :key="index" class="hover:bg-gray-50">
|
||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||
{{ formatDate(record.timestamp) }}
|
||
</td>
|
||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||
<span :class="getTypeBadgeClass(record.type)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
|
||
{{ record.type }}
|
||
</span>
|
||
</td>
|
||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm max-w-xs truncate" :title="record.details">
|
||
{{ record.details }}
|
||
</td>
|
||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||
<span :class="record.status === 'success' ? 'text-green-900 bg-green-200' : 'text-red-900 bg-red-200'" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
|
||
{{ record.status }}
|
||
</span>
|
||
</td>
|
||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||
<button v-if="record.result_path" @click="viewResult(record.result_path)" class="text-blue-600 hover:text-blue-900 mr-3">
|
||
查看
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="history.length === 0">
|
||
<td colspan="5" class="px-5 py-5 border-b border-gray-200 bg-white text-sm text-center text-gray-500">
|
||
暂无记录
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div class="mt-4 flex justify-end">
|
||
<button @click="fetchHistory" class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded text-sm">
|
||
<i class="fas fa-sync-alt"></i> 刷新
|
||
</button>
|
||
</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.path)" 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 like "results/..."
|
||
// We need to parse this. If it's a directory, go to files tab. If image, preview.
|
||
// For simplicity, let's assume it links to the folder in files tab
|
||
currentTab.value = 'files';
|
||
// Extract folder name from path if possible, or just go to root
|
||
const match = path.match(/results\/([^\/]+)/);
|
||
if (match) {
|
||
currentPath.value = match[1];
|
||
fetchFiles();
|
||
} else {
|
||
currentPath.value = '';
|
||
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 getTypeBadgeClass = (type) => {
|
||
const map = {
|
||
'general': 'bg-blue-100 text-blue-800',
|
||
'tarot': 'bg-purple-100 text-purple-800',
|
||
'face': 'bg-pink-100 text-pink-800'
|
||
};
|
||
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, getTypeBadgeClass, cleaning, deviceInfo,
|
||
currentModel, availableModels, updateModel,
|
||
cleanupConfig, saveCleanupConfig,
|
||
prompts, fetchPrompts, savePrompt, getPromptDescription
|
||
};
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html>
|