release: v2.1.0 - Prompt管理系统、页脚优化、图标修复

This commit is contained in:
ZJPB Admin
2025-12-30 01:17:08 +08:00
parent 9e47ebe749
commit 9f5d006090
23 changed files with 5871 additions and 99 deletions

View File

@@ -0,0 +1,370 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改密码 - ZJPB 焦提示词</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<!-- Google Material Symbols -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom Admin Theme -->
<link href="{{ url_for('static', filename='css/admin-sidebar.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/admin-actions.css') }}" rel="stylesheet">
</head>
<body class="admin-sidebar-layout">
<!-- 左侧菜单栏 -->
<aside class="admin-sidebar">
<!-- Logo -->
<div class="sidebar-logo">
<span class="material-symbols-outlined logo-icon">blur_on</span>
<span class="logo-text">ZJPB 焦提示词</span>
</div>
<!-- 主菜单 -->
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">主菜单</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="{{ url_for('admin.index') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">dashboard</span>
<span class="nav-text">控制台</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('site.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">public</span>
<span class="nav-text">网站管理</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('tag.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">label</span>
<span class="nav-text">标签管理</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('news.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">newspaper</span>
<span class="nav-text">新闻管理</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('admin_users.index_view') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">admin_panel_settings</span>
<span class="nav-text">管理员</span>
</a>
</li>
</ul>
</div>
<!-- 系统菜单 -->
<div class="nav-section">
<div class="nav-section-title">系统</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="{{ url_for('batch_import') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">upload_file</span>
<span class="nav-text">批量导入</span>
</a>
</li>
<li class="nav-item active">
<a href="{{ url_for('change_password') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">lock_reset</span>
<span class="nav-text">修改密码</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('index') }}" class="nav-link" target="_blank">
<span class="material-symbols-outlined nav-icon">open_in_new</span>
<span class="nav-text">查看网站</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('admin_logout') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">logout</span>
<span class="nav-text">退出登录</span>
</a>
</li>
</ul>
</div>
</nav>
<!-- 用户信息 -->
<div class="sidebar-user">
<div class="user-avatar">
<span class="material-symbols-outlined">account_circle</span>
</div>
<div class="user-info">
<div class="user-name">{{ current_user.username }}</div>
<div class="user-email">{{ current_user.email or 'admin@zjpb.com' }}</div>
</div>
</div>
</aside>
<!-- 右侧主内容区 -->
<div class="admin-main">
<!-- 顶部导航栏 -->
<header class="admin-header">
<div class="header-breadcrumb">
<a href="{{ url_for('admin.index') }}" class="breadcrumb-link">控制台</a>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-current">修改密码</span>
</div>
<div class="header-actions">
<div class="search-box">
<span class="material-symbols-outlined search-icon">search</span>
<input type="text" placeholder="全局搜索..." class="search-input">
</div>
<button class="header-btn">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="header-btn">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<!-- 页面内容 -->
<main class="admin-content">
<div class="page-header">
<div>
<h1 class="page-title">修改密码</h1>
<p class="page-description">修改您的管理员账户登录密码</p>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 8px;">
{% if category == 'error' %}error{% else %}check_circle{% endif %}
</span>
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="row">
<div class="col-md-8 col-lg-6">
<div class="card">
<div class="card-body">
<form method="POST" action="{{ url_for('change_password') }}">
<!-- 旧密码 -->
<div class="form-group">
<label for="old_password">旧密码</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<span class="material-symbols-outlined" style="font-size: 20px;">lock</span>
</span>
</div>
<input type="password"
class="form-control"
id="old_password"
name="old_password"
placeholder="请输入旧密码"
required
autofocus>
</div>
</div>
<!-- 新密码 -->
<div class="form-group">
<label for="new_password">新密码</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<span class="material-symbols-outlined" style="font-size: 20px;">lock_reset</span>
</span>
</div>
<input type="password"
class="form-control"
id="new_password"
name="new_password"
placeholder="请输入新密码至少6位"
required
minlength="6">
</div>
<small class="form-text text-muted">密码长度至少6位</small>
</div>
<!-- 确认新密码 -->
<div class="form-group">
<label for="confirm_password">确认新密码</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<span class="material-symbols-outlined" style="font-size: 20px;">check_circle</span>
</span>
</div>
<input type="password"
class="form-control"
id="confirm_password"
name="confirm_password"
placeholder="请再次输入新密码"
required
minlength="6">
</div>
</div>
<!-- 提交按钮 -->
<div class="form-group mb-0 mt-4">
<button type="submit" class="btn btn-primary btn-block">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 4px;">save</span>
确认修改
</button>
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary btn-block mt-2">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 4px;">cancel</span>
取消
</a>
</div>
</form>
</div>
</div>
<!-- 安全提示 -->
<div class="alert alert-info mt-3">
<span class="material-symbols-outlined" style="font-size: 18px; vertical-align: middle; margin-right: 8px;">info</span>
<strong>安全提示:</strong>
<ul class="mb-0 mt-2" style="padding-left: 20px;">
<li>密码修改成功后,您将被自动登出,需要使用新密码重新登录</li>
<li>请妥善保管您的密码,不要与他人分享</li>
<li>建议定期修改密码以保证账号安全</li>
</ul>
</div>
</div>
</div>
</main>
</div>
<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
<style>
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.page-description {
color: #666;
margin-bottom: 24px;
}
.card {
border: 1px solid #DCDFE6;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .05);
}
.card-body {
padding: 24px;
}
.form-group label {
color: #000000;
font-weight: 500;
margin-bottom: 8px;
}
.input-group-text {
background: #F5F7FA;
border: 1px solid #DCDFE6;
border-right: none;
color: #606266;
}
.form-control {
border: 1px solid #DCDFE6;
border-radius: 0 4px 4px 0;
padding: 10px 12px;
height: auto;
}
.form-control:focus {
border-color: #0052D9;
box-shadow: none;
}
.btn-primary {
background: #0052D9;
border-color: #0052D9;
padding: 10px 16px;
font-weight: 500;
}
.btn-primary:hover {
background: #003FA3;
border-color: #003FA3;
}
.btn-secondary {
background: #F5F7FA;
border-color: #DCDFE6;
color: #606266;
padding: 10px 16px;
font-weight: 500;
}
.btn-secondary:hover {
background: #E6E8EB;
border-color: #C0C4CC;
color: #303133;
}
.alert {
border-radius: 4px;
}
.alert-info {
background: #ECF5FF;
border-color: #B3D8FF;
color: #0052D9;
}
.alert-danger {
background: #FEF0F0;
border-color: #FBC4C4;
color: #D54941;
}
.alert-success {
background: #F0F9FF;
border-color: #C1E7C1;
color: #00A870;
}
</style>
<script>
// 验证两次密码是否一致
document.querySelector('form').addEventListener('submit', function(e) {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
e.preventDefault();
alert('两次输入的新密码不一致,请重新输入');
document.getElementById('confirm_password').focus();
}
});
</script>
</body>
</html>

View File

@@ -11,6 +11,28 @@
margin-top: 10px;
margin-bottom: 15px;
}
.generate-features-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.upload-logo-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.logo-preview {
margin-top: 10px;
max-width: 200px;
max-height: 200px;
display: none;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
}
.logo-preview img {
max-width: 100%;
max-height: 150px;
object-fit: contain;
}
.fetch-status {
margin-top: 10px;
padding: 10px;
@@ -47,6 +69,26 @@
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 标签输入框样式 */
.tag-input-wrapper {
margin-top: 10px;
}
.tag-input-field {
width: 100%;
padding: 8px 12px;
border: 1px solid #DCDFE6;
border-radius: 4px;
font-size: 14px;
}
.tag-input-field:focus {
outline: none;
border-color: #0052D9;
}
.tag-input-help {
margin-top: 5px;
font-size: 12px;
color: #606266;
}
</style>
<script>
@@ -130,9 +172,238 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// 在标签字段后添加"AI生成标签"按钮
const tagsField = document.querySelector('select[name="tags"]');
if (tagsField) {
// 在Logo字段后添加"上传Logo"功能
const logoField = document.querySelector('input[name="logo"]');
if (logoField) {
// 创建文件输入框
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
// 创建上传按钮
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-warning upload-logo-btn';
uploadBtn.innerHTML = '📁 上传Logo图片';
// 创建预览容器
const previewDiv = document.createElement('div');
previewDiv.className = 'logo-preview';
previewDiv.innerHTML = '<img src="" alt="Logo预览"><p style="margin-top:5px; font-size:12px; color:#666;">Logo预览</p>';
logoField.parentNode.appendChild(fileInput);
logoField.parentNode.appendChild(uploadBtn);
logoField.parentNode.appendChild(previewDiv);
// 点击按钮触发文件选择
uploadBtn.addEventListener('click', function() {
fileInput.click();
});
// 文件选择后自动上传
fileInput.addEventListener('change', function() {
const file = fileInput.files[0];
if (!file) return;
// 验证文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件!');
return;
}
// 验证文件大小限制5MB
if (file.size > 5 * 1024 * 1024) {
alert('图片文件不能超过5MB');
return;
}
// 上传文件
const formData = new FormData();
formData.append('logo', file);
uploadBtn.disabled = true;
uploadBtn.textContent = '上传中...';
fetch('/api/upload-logo', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 设置Logo字段值
logoField.value = data.path;
// 显示预览
const img = previewDiv.querySelector('img');
img.src = data.path;
previewDiv.style.display = 'block';
alert('✓ Logo上传成功');
} else {
alert('✗ ' + (data.message || '上传失败'));
}
})
.catch(error => {
console.error('Error:', error);
alert('✗ 上传失败,请重试');
})
.finally(() => {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '📁 上传Logo图片';
fileInput.value = '';
});
});
// 如果Logo字段有值显示预览
if (logoField.value) {
const img = previewDiv.querySelector('img');
img.src = logoField.value;
previewDiv.style.display = 'block';
}
// 监听Logo字段变化更新预览
logoField.addEventListener('input', function() {
if (logoField.value) {
const img = previewDiv.querySelector('img');
img.src = logoField.value;
previewDiv.style.display = 'block';
} else {
previewDiv.style.display = 'none';
}
});
}
// 处理标签字段 - 添加手动输入功能
const tagsSelect = document.querySelector('select[name="tags"]');
if (tagsSelect) {
// 隐藏原始的select字段
tagsSelect.style.display = 'none';
// 创建文本输入框
const tagInputWrapper = document.createElement('div');
tagInputWrapper.className = 'tag-input-wrapper';
const tagInput = document.createElement('input');
tagInput.type = 'text';
tagInput.className = 'tag-input-field';
tagInput.placeholder = '输入标签名称按回车添加AI工具、图像处理、免费';
const tagHelpText = document.createElement('div');
tagHelpText.className = 'tag-input-help';
tagHelpText.textContent = '💡 提示:输入标签名称后按回车键添加,可以添加多个标签。已选标签会自动添加到下方列表。';
const selectedTagsDiv = document.createElement('div');
selectedTagsDiv.className = 'selected-tags';
selectedTagsDiv.style.marginTop = '10px';
tagInputWrapper.appendChild(tagInput);
tagInputWrapper.appendChild(tagHelpText);
tagInputWrapper.appendChild(selectedTagsDiv);
tagsSelect.parentNode.insertBefore(tagInputWrapper, tagsSelect.nextSibling);
// 显示已选标签
function updateSelectedTags() {
selectedTagsDiv.innerHTML = '';
const selectedOptions = Array.from(tagsSelect.selectedOptions);
// 如果没有选中的标签,显示提示
if (selectedOptions.length === 0) {
selectedTagsDiv.innerHTML = '<span style="color:#999; font-size:12px;">暂无已选标签</span>';
return;
}
selectedOptions.forEach(option => {
const tag = document.createElement('span');
tag.style.cssText = 'display:inline-block; background:#0052D9; color:white; padding:4px 10px; margin:4px; border-radius:4px; font-size:12px;';
// 获取标签文本 - 兼容多种方式
let tagText = option.textContent || option.innerText || option.text || option.innerHTML || option.label || `标签${option.value}`;
// 处理 <Tag XXX> 格式,提取出实际标签名称
const match = tagText.match(/<Tag\s+(.+?)>/);
if (match) {
tagText = match[1]; // 提取标签名称
}
tag.innerHTML = tagText + ' <span style="cursor:pointer; margin-left:5px; font-weight:bold;" data-tag-id="' + option.value + '">×</span>';
selectedTagsDiv.appendChild(tag);
// 为删除按钮添加点击事件
const deleteBtn = tag.querySelector('span[data-tag-id]');
if (deleteBtn) {
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
option.selected = false;
updateSelectedTags();
});
}
});
}
// 添加标签
tagInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const tagName = tagInput.value.trim();
if (!tagName) {
return;
}
// 检查标签是否已存在
let existingOption = null;
for (let option of tagsSelect.options) {
if (option.text.toLowerCase() === tagName.toLowerCase()) {
existingOption = option;
break;
}
}
if (existingOption) {
// 选中已存在的标签
existingOption.selected = true;
} else {
// 创建新标签选项使用负数ID表示新标签
const newOption = document.createElement('option');
newOption.value = 'new_' + Date.now();
newOption.text = tagName;
newOption.selected = true;
newOption.setAttribute('data-new-tag', 'true');
tagsSelect.appendChild(newOption);
}
tagInput.value = '';
updateSelectedTags();
}
});
// 初始化显示
updateSelectedTags();
// 表单提交时处理新标签
const form = tagsSelect.closest('form');
form.addEventListener('submit', function(e) {
// 收集所有新标签名称
const newTags = [];
Array.from(tagsSelect.options).forEach(option => {
if (option.selected && option.hasAttribute('data-new-tag')) {
newTags.push(option.text);
}
});
// 如果有新标签,添加到隐藏字段
if (newTags.length > 0) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'new_tags';
hiddenInput.value = newTags.join(',');
form.appendChild(hiddenInput);
}
});
// 在标签字段后添加"AI生成标签"按钮
const generateBtn = document.createElement('button');
generateBtn.type = 'button';
generateBtn.className = 'btn btn-success generate-tags-btn';
@@ -141,8 +412,8 @@ document.addEventListener('DOMContentLoaded', function() {
const tagsStatusDiv = document.createElement('div');
tagsStatusDiv.className = 'tags-status';
tagsField.parentNode.appendChild(generateBtn);
tagsField.parentNode.appendChild(tagsStatusDiv);
tagInputWrapper.appendChild(generateBtn);
tagInputWrapper.appendChild(tagsStatusDiv);
generateBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
@@ -175,9 +446,31 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 显示生成的标签
const tagsText = data.tags.join(', ');
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n请在标签字段中手动选择或创建这些标签', 'success');
// 自动添加生成的标签
data.tags.forEach(tagName => {
// 检查是否已存在
let exists = false;
for (let option of tagsSelect.options) {
if (option.text.toLowerCase() === tagName.toLowerCase()) {
option.selected = true;
exists = true;
break;
}
}
// 如果不存在,创建新标签
if (!exists) {
const newOption = document.createElement('option');
newOption.value = 'new_' + Date.now() + '_' + Math.random();
newOption.text = tagName;
newOption.selected = true;
newOption.setAttribute('data-new-tag', 'true');
tagsSelect.appendChild(newOption);
}
});
updateSelectedTags();
showTagsStatus('✓ AI已自动添加推荐标签' + data.tags.join(', '), 'success');
} else {
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
}
@@ -198,6 +491,152 @@ document.addEventListener('DOMContentLoaded', function() {
tagsStatusDiv.style.display = 'block';
}
}
// 在Description字段后添加"AI生成详细介绍"按钮
const descriptionField = document.querySelector('textarea[name="description"]');
if (descriptionField) {
// 创建生成按钮
const generateDescBtn = document.createElement('button');
generateDescBtn.type = 'button';
generateDescBtn.className = 'btn btn-success generate-features-btn';
generateDescBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成详细介绍';
const descStatusDiv = document.createElement('div');
descStatusDiv.className = 'tags-status';
descriptionField.parentNode.appendChild(generateDescBtn);
descriptionField.parentNode.appendChild(descStatusDiv);
generateDescBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const shortDescField = document.querySelector('input[name="short_desc"]');
const urlField = document.querySelector('input[name="url"]');
const name = nameField ? nameField.value.trim() : '';
const shortDesc = shortDescField ? shortDescField.value.trim() : '';
const url = urlField ? urlField.value.trim() : '';
if (!name) {
showDescStatus('请先填写网站名称', 'error');
return;
}
// 显示加载状态
generateDescBtn.disabled = true;
generateDescBtn.classList.add('loading');
descStatusDiv.style.display = 'none';
// 调用API生成详细介绍
fetch('/api/generate-description', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
short_desc: shortDesc,
url: url
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.description) {
// 自动填充到description字段
descriptionField.value = data.description;
showDescStatus('✓ AI已生成详细介绍', 'success');
} else {
showDescStatus('✗ ' + (data.message || '详细介绍生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showDescStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateDescBtn.disabled = false;
generateDescBtn.classList.remove('loading');
});
});
function showDescStatus(message, type) {
descStatusDiv.textContent = message;
descStatusDiv.className = 'tags-status ' + type;
descStatusDiv.style.display = 'block';
}
}
// 在Features字段后添加"AI生成功能"按钮
const featuresField = document.querySelector('textarea[name="features"]');
if (featuresField) {
// 创建生成按钮
const generateFeaturesBtn = document.createElement('button');
generateFeaturesBtn.type = 'button';
generateFeaturesBtn.className = 'btn btn-success generate-features-btn';
generateFeaturesBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成主要功能';
const featuresStatusDiv = document.createElement('div');
featuresStatusDiv.className = 'tags-status';
featuresField.parentNode.appendChild(generateFeaturesBtn);
featuresField.parentNode.appendChild(featuresStatusDiv);
generateFeaturesBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const descriptionField = document.querySelector('textarea[name="description"]');
const urlField = document.querySelector('input[name="url"]');
const name = nameField ? nameField.value.trim() : '';
const description = descriptionField ? descriptionField.value.trim() : '';
const url = urlField ? urlField.value.trim() : '';
if (!name || !description) {
showFeaturesStatus('请先填写网站名称和描述', 'error');
return;
}
// 显示加载状态
generateFeaturesBtn.disabled = true;
generateFeaturesBtn.classList.add('loading');
featuresStatusDiv.style.display = 'none';
// 调用API生成功能
fetch('/api/generate-features', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description,
url: url
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.features) {
// 自动填充到features字段
featuresField.value = data.features;
showFeaturesStatus('✓ AI已生成主要功能列表', 'success');
} else {
showFeaturesStatus('✗ ' + (data.message || '功能生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showFeaturesStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateFeaturesBtn.disabled = false;
generateFeaturesBtn.classList.remove('loading');
});
});
function showFeaturesStatus(message, type) {
featuresStatusDiv.textContent = message;
featuresStatusDiv.className = 'tags-status ' + type;
featuresStatusDiv.style.display = 'block';
}
}
});
</script>
{% endblock %}

View File

@@ -11,6 +11,28 @@
margin-top: 10px;
margin-bottom: 15px;
}
.generate-features-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.upload-logo-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.logo-preview {
margin-top: 10px;
max-width: 200px;
max-height: 200px;
display: none;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
}
.logo-preview img {
max-width: 100%;
max-height: 150px;
object-fit: contain;
}
.fetch-status {
margin-top: 10px;
padding: 10px;
@@ -47,6 +69,26 @@
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 标签输入框样式 */
.tag-input-wrapper {
margin-top: 10px;
}
.tag-input-field {
width: 100%;
padding: 8px 12px;
border: 1px solid #DCDFE6;
border-radius: 4px;
font-size: 14px;
}
.tag-input-field:focus {
outline: none;
border-color: #0052D9;
}
.tag-input-help {
margin-top: 5px;
font-size: 12px;
color: #606266;
}
</style>
<script>
@@ -130,72 +172,490 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
// 在标签字段后添加"AI生成标签"按钮
const tagsField = document.querySelector('select[name="tags"]');
if (tagsField) {
const generateBtn = document.createElement('button');
generateBtn.type = 'button';
generateBtn.className = 'btn btn-success generate-tags-btn';
generateBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成标签';
// 在Logo字段后添加"上传Logo"功能
const logoField = document.querySelector('input[name="logo"]');
if (logoField) {
// 创建文件输入框
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
const tagsStatusDiv = document.createElement('div');
tagsStatusDiv.className = 'tags-status';
// 创建上传按钮
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-warning upload-logo-btn';
uploadBtn.innerHTML = '📁 上传Logo图片';
tagsField.parentNode.appendChild(generateBtn);
tagsField.parentNode.appendChild(tagsStatusDiv);
// 创建预览容器
const previewDiv = document.createElement('div');
previewDiv.className = 'logo-preview';
previewDiv.innerHTML = '<img src="" alt="Logo预览"><p style="margin-top:5px; font-size:12px; color:#666;">Logo预览</p>';
generateBtn.addEventListener('click', function() {
logoField.parentNode.appendChild(fileInput);
logoField.parentNode.appendChild(uploadBtn);
logoField.parentNode.appendChild(previewDiv);
// 点击按钮触发文件选择
uploadBtn.addEventListener('click', function() {
fileInput.click();
});
// 文件选择后自动上传
fileInput.addEventListener('change', function() {
const file = fileInput.files[0];
if (!file) return;
// 验证文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件!');
return;
}
// 验证文件大小限制5MB
if (file.size > 5 * 1024 * 1024) {
alert('图片文件不能超过5MB');
return;
}
// 上传文件
const formData = new FormData();
formData.append('logo', file);
uploadBtn.disabled = true;
uploadBtn.textContent = '上传中...';
fetch('/api/upload-logo', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 设置Logo字段值
logoField.value = data.path;
// 显示预览
const img = previewDiv.querySelector('img');
img.src = data.path;
previewDiv.style.display = 'block';
alert('✓ Logo上传成功');
} else {
alert('✗ ' + (data.message || '上传失败'));
}
})
.catch(error => {
console.error('Error:', error);
alert('✗ 上传失败,请重试');
})
.finally(() => {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '📁 上传Logo图片';
fileInput.value = '';
});
});
// 如果Logo字段有值显示预览
if (logoField.value) {
const img = previewDiv.querySelector('img');
img.src = logoField.value;
previewDiv.style.display = 'block';
}
// 监听Logo字段变化更新预览
logoField.addEventListener('input', function() {
if (logoField.value) {
const img = previewDiv.querySelector('img');
img.src = logoField.value;
previewDiv.style.display = 'block';
} else {
previewDiv.style.display = 'none';
}
});
}
// 处理标签字段 - 添加手动输入功能
const tagsSelect = document.querySelector('select[name="tags"]');
if (tagsSelect) {
// 先等待一下,确保 Flask-Admin 已经初始化好 select
setTimeout(function() {
// 保存原始选中的标签(从数据库加载的)
const originalSelectedOptions = Array.from(tagsSelect.selectedOptions);
// 隐藏原始的select字段
tagsSelect.style.display = 'none';
// 创建文本输入框
const tagInputWrapper = document.createElement('div');
tagInputWrapper.className = 'tag-input-wrapper';
const tagInput = document.createElement('input');
tagInput.type = 'text';
tagInput.className = 'tag-input-field';
tagInput.placeholder = '输入标签名称按回车添加AI工具、图像处理、免费';
const tagHelpText = document.createElement('div');
tagHelpText.className = 'tag-input-help';
tagHelpText.textContent = '💡 提示:输入标签名称后按回车键添加,可以添加多个标签。已选标签会自动添加到下方列表。';
const selectedTagsDiv = document.createElement('div');
selectedTagsDiv.className = 'selected-tags';
selectedTagsDiv.style.marginTop = '10px';
tagInputWrapper.appendChild(tagInput);
tagInputWrapper.appendChild(tagHelpText);
tagInputWrapper.appendChild(selectedTagsDiv);
tagsSelect.parentNode.insertBefore(tagInputWrapper, tagsSelect.nextSibling);
// 显示已选标签
function updateSelectedTags() {
selectedTagsDiv.innerHTML = '';
const selectedOptions = Array.from(tagsSelect.selectedOptions);
// 如果没有选中的标签,显示提示
if (selectedOptions.length === 0) {
selectedTagsDiv.innerHTML = '<span style="color:#999; font-size:12px;">暂无已选标签</span>';
return;
}
selectedOptions.forEach(option => {
const tag = document.createElement('span');
tag.style.cssText = 'display:inline-block; background:#0052D9; color:white; padding:4px 10px; margin:4px; border-radius:4px; font-size:12px;';
// 获取标签文本 - 兼容多种方式
let tagText = option.textContent || option.innerText || option.text || option.innerHTML || option.label || `标签${option.value}`;
// 处理 <Tag XXX> 格式,提取出实际标签名称
const match = tagText.match(/<Tag\s+(.+?)>/);
if (match) {
tagText = match[1]; // 提取标签名称
}
tag.innerHTML = tagText + ' <span style="cursor:pointer; margin-left:5px; font-weight:bold;" data-tag-id="' + option.value + '">×</span>';
selectedTagsDiv.appendChild(tag);
// 为删除按钮添加点击事件
const deleteBtn = tag.querySelector('span[data-tag-id]');
if (deleteBtn) {
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
option.selected = false;
updateSelectedTags();
});
}
});
}
// 添加标签
tagInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const tagName = tagInput.value.trim();
if (!tagName) {
return;
}
// 检查标签是否已存在
let existingOption = null;
for (let option of tagsSelect.options) {
if (option.text.toLowerCase() === tagName.toLowerCase()) {
existingOption = option;
break;
}
}
if (existingOption) {
// 选中已存在的标签
existingOption.selected = true;
} else {
// 创建新标签选项使用负数ID表示新标签
const newOption = document.createElement('option');
newOption.value = 'new_' + Date.now();
newOption.text = tagName;
newOption.selected = true;
newOption.setAttribute('data-new-tag', 'true');
tagsSelect.appendChild(newOption);
}
tagInput.value = '';
updateSelectedTags();
}
});
// 初始化显示 - 使用延迟确保数据已加载
updateSelectedTags();
// 再次确保原始选中的标签保持选中状态
originalSelectedOptions.forEach(opt => {
// 在所有选项中找到对应的选项并设置为选中
for (let option of tagsSelect.options) {
if (option.value === opt.value) {
option.selected = true;
}
}
});
// 再次更新显示
setTimeout(function() {
updateSelectedTags();
}, 100);
// 表单提交时处理新标签
const form = tagsSelect.closest('form');
form.addEventListener('submit', function(e) {
// 收集所有新标签名称
const newTags = [];
Array.from(tagsSelect.options).forEach(option => {
if (option.selected && option.hasAttribute('data-new-tag')) {
newTags.push(option.text);
}
});
// 如果有新标签,添加到隐藏字段
if (newTags.length > 0) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'new_tags';
hiddenInput.value = newTags.join(',');
form.appendChild(hiddenInput);
}
});
// 在标签字段后添加"AI生成标签"按钮
const generateBtn = document.createElement('button');
generateBtn.type = 'button';
generateBtn.className = 'btn btn-success generate-tags-btn';
generateBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成标签';
const tagsStatusDiv = document.createElement('div');
tagsStatusDiv.className = 'tags-status';
tagInputWrapper.appendChild(generateBtn);
tagInputWrapper.appendChild(tagsStatusDiv);
generateBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const descriptionField = document.querySelector('textarea[name="description"]');
const name = nameField ? nameField.value.trim() : '';
const description = descriptionField ? descriptionField.value.trim() : '';
if (!name || !description) {
showTagsStatus('请先填写网站名称和描述', 'error');
return;
}
// 显示加载状态
generateBtn.disabled = true;
generateBtn.classList.add('loading');
tagsStatusDiv.style.display = 'none';
// 调用API生成标签
fetch('/api/generate-tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 自动添加生成的标签
data.tags.forEach(tagName => {
// 检查是否已存在
let exists = false;
for (let option of tagsSelect.options) {
if (option.text.toLowerCase() === tagName.toLowerCase()) {
option.selected = true;
exists = true;
break;
}
}
// 如果不存在,创建新标签
if (!exists) {
const newOption = document.createElement('option');
newOption.value = 'new_' + Date.now() + '_' + Math.random();
newOption.text = tagName;
newOption.selected = true;
newOption.setAttribute('data-new-tag', 'true');
tagsSelect.appendChild(newOption);
}
});
updateSelectedTags();
showTagsStatus('✓ AI已自动添加推荐标签' + data.tags.join(', '), 'success');
} else {
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showTagsStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateBtn.disabled = false;
generateBtn.classList.remove('loading');
});
});
function showTagsStatus(message, type) {
tagsStatusDiv.textContent = message;
tagsStatusDiv.className = 'tags-status ' + type;
tagsStatusDiv.style.display = 'block';
}
}, 50); // setTimeout 结束
}
// 在Description字段后添加"AI生成详细介绍"按钮
const descriptionField = document.querySelector('textarea[name="description"]');
if (descriptionField) {
// 创建生成按钮
const generateDescBtn = document.createElement('button');
generateDescBtn.type = 'button';
generateDescBtn.className = 'btn btn-success generate-features-btn';
generateDescBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成详细介绍';
const descStatusDiv = document.createElement('div');
descStatusDiv.className = 'tags-status';
descriptionField.parentNode.appendChild(generateDescBtn);
descriptionField.parentNode.appendChild(descStatusDiv);
generateDescBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const descriptionField = document.querySelector('textarea[name="description"]');
const shortDescField = document.querySelector('input[name="short_desc"]');
const urlField = document.querySelector('input[name="url"]');
const name = nameField ? nameField.value.trim() : '';
const description = descriptionField ? descriptionField.value.trim() : '';
const shortDesc = shortDescField ? shortDescField.value.trim() : '';
const url = urlField ? urlField.value.trim() : '';
if (!name || !description) {
showTagsStatus('请先填写网站名称和描述', 'error');
if (!name) {
showDescStatus('请先填写网站名称', 'error');
return;
}
// 显示加载状态
generateBtn.disabled = true;
generateBtn.classList.add('loading');
tagsStatusDiv.style.display = 'none';
generateDescBtn.disabled = true;
generateDescBtn.classList.add('loading');
descStatusDiv.style.display = 'none';
// 调用API生成标签
fetch('/api/generate-tags', {
// 调用API生成详细介绍
fetch('/api/generate-description', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description
short_desc: shortDesc,
url: url
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.tags && data.tags.length > 0) {
// 显示生成的标签
const tagsText = data.tags.join(', ');
showTagsStatus('✓ AI生成的标签建议: ' + tagsText + '\n请在标签字段中手动选择或创建这些标签', 'success');
if (data.success && data.description) {
// 自动填充到description字段
descriptionField.value = data.description;
showDescStatus('✓ AI生成详细介绍', 'success');
} else {
showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
showDescStatus('✗ ' + (data.message || '详细介绍生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showTagsStatus('✗ 网络请求失败,请重试', 'error');
showDescStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateBtn.disabled = false;
generateBtn.classList.remove('loading');
generateDescBtn.disabled = false;
generateDescBtn.classList.remove('loading');
});
});
function showTagsStatus(message, type) {
tagsStatusDiv.textContent = message;
tagsStatusDiv.className = 'tags-status ' + type;
tagsStatusDiv.style.display = 'block';
function showDescStatus(message, type) {
descStatusDiv.textContent = message;
descStatusDiv.className = 'tags-status ' + type;
descStatusDiv.style.display = 'block';
}
}
// 在Features字段后添加"AI生成功能"按钮
const featuresField = document.querySelector('textarea[name="features"]');
if (featuresField) {
// 创建生成按钮
const generateFeaturesBtn = document.createElement('button');
generateFeaturesBtn.type = 'button';
generateFeaturesBtn.className = 'btn btn-success generate-features-btn';
generateFeaturesBtn.innerHTML = '<span class="normal-icon">✨</span><span class="loading-icon">↻</span> AI生成主要功能';
const featuresStatusDiv = document.createElement('div');
featuresStatusDiv.className = 'tags-status';
featuresField.parentNode.appendChild(generateFeaturesBtn);
featuresField.parentNode.appendChild(featuresStatusDiv);
generateFeaturesBtn.addEventListener('click', function() {
const nameField = document.querySelector('input[name="name"]');
const descriptionField = document.querySelector('textarea[name="description"]');
const urlField = document.querySelector('input[name="url"]');
const name = nameField ? nameField.value.trim() : '';
const description = descriptionField ? descriptionField.value.trim() : '';
const url = urlField ? urlField.value.trim() : '';
if (!name || !description) {
showFeaturesStatus('请先填写网站名称和描述', 'error');
return;
}
// 显示加载状态
generateFeaturesBtn.disabled = true;
generateFeaturesBtn.classList.add('loading');
featuresStatusDiv.style.display = 'none';
// 调用API生成功能
fetch('/api/generate-features', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description,
url: url
})
})
.then(response => response.json())
.then(data => {
if (data.success && data.features) {
// 自动填充到features字段
featuresField.value = data.features;
showFeaturesStatus('✓ AI已生成主要功能列表', 'success');
} else {
showFeaturesStatus('✗ ' + (data.message || '功能生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showFeaturesStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
generateFeaturesBtn.disabled = false;
generateFeaturesBtn.classList.remove('loading');
});
});
function showFeaturesStatus(message, type) {
featuresStatusDiv.textContent = message;
featuresStatusDiv.className = 'tags-status ' + type;
featuresStatusDiv.style.display = 'block';
}
}
});