Files
feishu_screen/templates/index.html

829 lines
25 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Screen2Feishu - AI任务管理</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
body {
background: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: #2c3e50;
color: white;
padding: 20px 0;
margin-bottom: 30px;
}
header h1 {
font-size: 24px;
font-weight: 600;
text-align: center;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card h2 {
font-size: 18px;
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
input[type="text"],
input[type="datetime-local"],
select,
input[type="file"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
input[type="text"]:focus,
input[type="datetime-local"]:focus,
select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
width: auto;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #219653;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.status-section {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
.status-section h3 {
font-size: 16px;
margin-bottom: 10px;
color: #2c3e50;
}
.status-item {
margin-bottom: 8px;
padding: 8px;
background: white;
border-radius: 4px;
border-left: 3px solid #3498db;
}
.status-item.success {
border-left-color: #27ae60;
background: #e8f5e9;
}
.status-item.warning {
border-left-color: #f39c12;
background: #fff3e0;
}
.status-item.error {
border-left-color: #e74c3c;
background: #ffebee;
}
.ai-result {
background: #e3f2fd;
padding: 15px;
border-radius: 4px;
margin-top: 15px;
font-size: 14px;
}
.ai-result h4 {
margin-bottom: 10px;
color: #1976d2;
}
.ai-result p {
margin-bottom: 5px;
}
.ai-result .highlight {
font-weight: 600;
color: #d32f2f;
}
.config-info {
background: #f3e5f5;
padding: 15px;
border-radius: 4px;
margin-top: 15px;
font-size: 13px;
}
.config-info h4 {
margin-bottom: 10px;
color: #7b1fa2;
}
.config-item {
margin-bottom: 5px;
}
.config-item span {
font-weight: 500;
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background: #2c3e50;
color: white;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 1000;
max-width: 400px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.success {
background: #27ae60;
}
.notification.error {
background: #e74c3c;
}
.notification.warning {
background: #f39c12;
}
.file-upload-area {
border: 2px dashed #ddd;
border-radius: 4px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 15px;
}
.file-upload-area:hover {
border-color: #3498db;
background: #f0f8ff;
}
.file-upload-area.dragover {
border-color: #3498db;
background: #e3f2fd;
}
.file-upload-area p {
color: #666;
margin: 10px 0;
}
.file-upload-area i {
font-size: 24px;
color: #3498db;
}
.hidden {
display: none;
}
.stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stat-item {
background: white;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
flex: 1;
min-width: 150px;
}
.stat-item h3 {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.stat-item p {
font-size: 24px;
font-weight: 600;
color: #2c3e50;
}
.progress-bar {
width: 100%;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 10px;
}
.progress-fill {
height: 100%;
background: #3498db;
width: 0%;
transition: width 0.3s;
}
.section-divider {
height: 1px;
background: #e0e0e0;
margin: 20px 0;
}
.info-text {
font-size: 13px;
color: #666;
margin-top: 10px;
line-height: 1.5;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
margin-left: 5px;
}
.badge.success {
background: #e8f5e9;
color: #2e7d32;
}
.badge.warning {
background: #fff3e0;
color: #ef6c00;
}
.badge.error {
background: #ffebee;
color: #c62828;
}
.badge.info {
background: #e3f2fd;
color: #1565c0;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Screen2Feishu - AI任务管理</h1>
</div>
</header>
<div class="container">
<!-- 统计信息 -->
<div class="stats" id="statsSection">
<div class="stat-item">
<h3>用户缓存</h3>
<p id="userCount">-</p>
</div>
<div class="stat-item">
<h3>最近联系人</h3>
<p id="recentCount">-</p>
</div>
<div class="stat-item">
<h3>缓存状态</h3>
<p id="cacheStatus">-</p>
</div>
</div>
<div class="main-content">
<!-- 左侧:图片上传 -->
<div class="card">
<h2>📸 图片上传分析</h2>
<div class="file-upload-area" id="uploadArea">
<i>📁</i>
<p>点击选择图片或拖拽到此处</p>
<p style="font-size: 12px; color: #999;">支持 JPG, PNG, GIF 等格式</p>
<input type="file" id="imageInput" accept="image/*" class="hidden">
</div>
<div class="form-group">
<label>文件名:</label>
<input type="text" id="fileName" readonly placeholder="未选择文件">
</div>
<div class="btn-group">
<button class="btn btn-success" id="uploadBtn" disabled>
<span id="uploadBtnText">上传并分析</span>
</button>
<button class="btn btn-primary" id="clearBtn">清空</button>
</div>
<div class="status-section" id="uploadStatus" style="display: none;">
<h3>上传状态</h3>
<div id="uploadStatusContent"></div>
</div>
<div class="ai-result" id="aiResult" style="display: none;">
<h4>AI分析结果</h4>
<div id="aiResultContent"></div>
</div>
</div>
<!-- 右侧:配置信息和状态 -->
<div class="card">
<h2>⚙️ 系统配置</h2>
<div class="config-info" id="configInfo">
<h4>当前配置</h4>
<div class="config-item">内存处理: <span id="configMemory">-</span></div>
<div class="config-item">用户匹配: <span id="configUserMatch">-</span></div>
<div class="config-item">随机匹配: <span id="configRandom">-</span></div>
<div class="config-item">缓存功能: <span id="configCache">-</span></div>
<div class="config-item">文件处理: <span id="configPostProcess">-</span></div>
</div>
<div class="section-divider"></div>
<h2>📊 操作说明</h2>
<div class="info-text">
<p><strong>1. 图片上传:</strong> 选择截图文件,系统会自动分析图片内容</p>
<p><strong>2. AI分析:</strong> 识别任务描述、优先级、发起人等信息</p>
<p><strong>3. 用户匹配:</strong> 自动匹配飞书用户,处理重名情况</p>
<p><strong>4. 写入飞书:</strong> 将分析结果写入飞书多维表格</p>
<p><strong>5. 结果反馈:</strong> 显示AI分析结果和操作状态</p>
</div>
<div class="section-divider"></div>
<h2>📝 状态日志</h2>
<div class="status-section" id="logSection">
<div id="logContent"></div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let currentFile = null;
let isUploading = false;
// DOM元素
const uploadArea = document.getElementById('uploadArea');
const imageInput = document.getElementById('imageInput');
const fileName = document.getElementById('fileName');
const uploadBtn = document.getElementById('uploadBtn');
const uploadBtnText = document.getElementById('uploadBtnText');
const clearBtn = document.getElementById('clearBtn');
const uploadStatus = document.getElementById('uploadStatus');
const uploadStatusContent = document.getElementById('uploadStatusContent');
const aiResult = document.getElementById('aiResult');
const aiResultContent = document.getElementById('aiResultContent');
const logContent = document.getElementById('logContent');
// 统计元素
const userCount = document.getElementById('userCount');
const recentCount = document.getElementById('recentCount');
const cacheStatus = document.getElementById('cacheStatus');
// 配置元素
const configMemory = document.getElementById('configMemory');
const configUserMatch = document.getElementById('configUserMatch');
const configRandom = document.getElementById('configRandom');
const configCache = document.getElementById('configCache');
const configPostProcess = document.getElementById('configPostProcess');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadConfig();
loadUserCache();
setupEventListeners();
addLog('系统已启动,等待操作...');
});
// 设置事件监听器
function setupEventListeners() {
// 文件上传区域点击
uploadArea.addEventListener('click', () => {
if (!isUploading) {
imageInput.click();
}
});
// 文件选择变化
imageInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
// 拖拽上传
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (file.type.startsWith('image/')) {
handleFileSelect(file);
} else {
showNotification('请选择图片文件', 'error');
}
}
});
// 上传按钮
uploadBtn.addEventListener('click', uploadImage);
// 清空按钮
clearBtn.addEventListener('click', clearForm);
}
// 处理文件选择
function handleFileSelect(file) {
currentFile = file;
fileName.value = file.name;
uploadBtn.disabled = false;
addLog(`已选择文件: ${file.name} (${formatFileSize(file.size)})`);
showNotification('文件已选择,点击上传分析', 'success');
}
// 上传图片并分析
async function uploadImage() {
if (!currentFile || isUploading) {
return;
}
isUploading = true;
uploadBtn.disabled = true;
uploadBtnText.innerHTML = '<span class="loading"></span>分析中...';
// 显示上传状态
uploadStatus.style.display = 'block';
uploadStatusContent.innerHTML = '<div class="status-item">正在上传图片...</div>';
try {
// 创建FormData
const formData = new FormData();
formData.append('image', currentFile);
// 调用后端API
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
// 显示成功状态
uploadStatusContent.innerHTML = '<div class="status-item success">上传成功,正在分析...</div>';
addLog(`图片上传成功: ${currentFile.name}`);
// 显示AI分析结果
if (result.ai_result) {
displayAIResult(result.ai_result);
addLog('AI分析完成结果已显示');
}
// 显示成功消息
showNotification(result.message, 'success');
addLog(`任务已成功添加到飞书`);
// 清空文件选择
clearForm();
} else {
// 显示错误状态
uploadStatusContent.innerHTML = `<div class="status-item error">分析失败: ${result.error}</div>`;
addLog(`分析失败: ${result.error}`, 'error');
showNotification(`分析失败: ${result.error}`, 'error');
}
} catch (error) {
// 显示错误状态
uploadStatusContent.innerHTML = `<div class="status-item error">网络错误: ${error.message}</div>`;
addLog(`网络错误: ${error.message}`, 'error');
showNotification(`网络错误: ${error.message}`, 'error');
} finally {
isUploading = false;
uploadBtn.disabled = false;
uploadBtnText.textContent = '上传并分析';
}
}
// 显示AI分析结果
function displayAIResult(aiResult) {
let html = '';
// 任务描述
if (aiResult.task_description) {
html += `<p><strong>任务描述:</strong> ${aiResult.task_description}</p>`;
}
// 优先级
if (aiResult.priority) {
const priorityClass = getPriorityClass(aiResult.priority);
html += `<p><strong>优先级:</strong> <span class="badge ${priorityClass}">${aiResult.priority}</span></p>`;
}
// 状态
if (aiResult.status) {
html += `<p><strong>状态:</strong> ${aiResult.status}</p>`;
}
// 发起人
if (aiResult.initiator) {
html += `<p><strong>发起人:</strong> ${aiResult.initiator}</p>`;
}
// 部门
if (aiResult.department) {
html += `<p><strong>部门:</strong> ${aiResult.department}</p>`;
}
// 任务发起方(匹配结果)
if (aiResult["任务发起方"]) {
html += `<p><strong>匹配用户:</strong> ${aiResult["任务发起方"].id || '未匹配'}</p>`;
}
// 任务发起方.部门(匹配结果)
if (aiResult["任务发起方.部门"]) {
html += `<p><strong>匹配部门:</strong> ${aiResult["任务发起方.部门"]}</p>`;
}
// 日期
if (aiResult.start_date) {
html += `<p><strong>开始日期:</strong> ${aiResult.start_date}</p>`;
}
if (aiResult.due_date) {
html += `<p><strong>截止日期:</strong> ${aiResult.due_date}</p>`;
}
aiResultContent.innerHTML = html;
aiResult.style.display = 'block';
}
// 获取优先级样式类
function getPriorityClass(priority) {
const priorityMap = {
'紧急': 'error',
'较紧急': 'warning',
'一般': 'info',
'普通': 'success'
};
return priorityMap[priority] || 'info';
}
// 清空表单
function clearForm() {
currentFile = null;
imageInput.value = '';
fileName.value = '';
uploadBtn.disabled = true;
uploadStatus.style.display = 'none';
aiResult.style.display = 'none';
addLog('表单已清空');
}
// 加载配置
async function loadConfig() {
try {
const response = await fetch('/api/config');
const result = await response.json();
if (result.success) {
const config = result.config;
configMemory.textContent = config.memory_processing ? '启用' : '禁用';
configUserMatch.textContent = config.user_matching_enabled ? '启用' : '禁用';
configRandom.textContent = config.fallback_to_random ? '启用' : '禁用';
configCache.textContent = config.cache_enabled ? '启用' : '禁用';
configPostProcess.textContent = config.post_process;
addLog('配置加载成功');
} else {
addLog(`加载配置失败: ${result.error}`, 'error');
}
} catch (error) {
addLog(`加载配置失败: ${error.message}`, 'error');
}
}
// 加载用户缓存信息
async function loadUserCache() {
try {
const response = await fetch('/api/user_cache');
const result = await response.json();
if (result.success) {
const stats = result.stats;
userCount.textContent = stats.user_count || 0;
recentCount.textContent = stats.recent_contact_count || 0;
cacheStatus.textContent = stats.is_expired ? '已过期' : '有效';
if (stats.cache_age_hours > 0) {
cacheStatus.textContent += ` (${stats.cache_age_hours.toFixed(1)}小时)`;
}
addLog('用户缓存信息加载成功');
} else {
addLog(`加载用户缓存失败: ${result.error}`, 'error');
}
} catch (error) {
addLog(`加载用户缓存失败: ${error.message}`, 'error');
}
}
// 添加日志
function addLog(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `status-item ${type}`;
logEntry.innerHTML = `<strong>[${timestamp}]</strong> ${message}`;
logContent.insertBefore(logEntry, logContent.firstChild);
// 限制日志数量
while (logContent.children.length > 20) {
logContent.removeChild(logContent.lastChild);
}
}
// 显示通知
function showNotification(message, type = 'info') {
// 移除现有通知
const existingNotification = document.querySelector('.notification');
if (existingNotification) {
existingNotification.remove();
}
// 创建新通知
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 3000);
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 定期更新用户缓存信息
setInterval(() => {
loadUserCache();
}, 30000); // 每30秒更新一次
</script>
</body>
</html>