2025-12-27 22:45:09 +08:00
|
|
|
|
{% extends 'admin/model/edit.html' %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block tail %}
|
|
|
|
|
|
{{ super() }}
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.auto-fetch-btn {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
.generate-tags-btn {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
2025-12-30 00:45:39 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2025-12-27 22:45:09 +08:00
|
|
|
|
.fetch-status {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
.tags-status {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.fetch-status.success, .tags-status.success {
|
2025-12-27 22:45:09 +08:00
|
|
|
|
background-color: rgba(34, 197, 94, 0.1);
|
|
|
|
|
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
|
|
|
|
color: #4ade80;
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
.fetch-status.error, .tags-status.error {
|
2025-12-27 22:45:09 +08:00
|
|
|
|
background-color: rgba(239, 68, 68, 0.1);
|
|
|
|
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
|
|
|
|
color: #f87171;
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
.auto-fetch-btn .loading-icon, .generate-tags-btn .loading-icon {
|
2025-12-27 22:45:09 +08:00
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
.auto-fetch-btn.loading .loading-icon, .generate-tags-btn.loading .loading-icon {
|
2025-12-27 22:45:09 +08:00
|
|
|
|
display: inline-block;
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
.auto-fetch-btn.loading .normal-icon, .generate-tags-btn.loading .normal-icon {
|
2025-12-27 22:45:09 +08:00
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
from { transform: rotate(0deg); }
|
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
2025-12-30 00:45:39 +08:00
|
|
|
|
/* 标签输入框样式 */
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2025-12-27 22:45:09 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
// 在URL字段后添加"自动获取"按钮
|
|
|
|
|
|
const urlField = document.querySelector('input[name="url"]');
|
|
|
|
|
|
if (urlField) {
|
|
|
|
|
|
const fetchBtn = document.createElement('button');
|
|
|
|
|
|
fetchBtn.type = 'button';
|
|
|
|
|
|
fetchBtn.className = 'btn btn-info auto-fetch-btn';
|
|
|
|
|
|
fetchBtn.innerHTML = '<span class="normal-icon">↻</span><span class="loading-icon">↻</span> 自动获取网站信息';
|
|
|
|
|
|
|
|
|
|
|
|
const statusDiv = document.createElement('div');
|
|
|
|
|
|
statusDiv.className = 'fetch-status';
|
|
|
|
|
|
|
|
|
|
|
|
urlField.parentNode.appendChild(fetchBtn);
|
|
|
|
|
|
urlField.parentNode.appendChild(statusDiv);
|
|
|
|
|
|
|
|
|
|
|
|
fetchBtn.addEventListener('click', function() {
|
|
|
|
|
|
const url = urlField.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!url) {
|
|
|
|
|
|
showStatus('请先输入网站URL', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示加载状态
|
|
|
|
|
|
fetchBtn.disabled = true;
|
|
|
|
|
|
fetchBtn.classList.add('loading');
|
|
|
|
|
|
statusDiv.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
// 调用API获取网站信息
|
|
|
|
|
|
fetch('/api/fetch-website-info', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ url: url })
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
// 自动填充表单字段
|
|
|
|
|
|
const nameField = document.querySelector('input[name="name"]');
|
|
|
|
|
|
const shortDescField = document.querySelector('input[name="short_desc"]');
|
|
|
|
|
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
|
|
|
|
|
const logoField = document.querySelector('input[name="logo"]');
|
|
|
|
|
|
|
|
|
|
|
|
if (nameField && data.data.name) {
|
|
|
|
|
|
nameField.value = data.data.name;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (shortDescField && data.data.description) {
|
|
|
|
|
|
shortDescField.value = data.data.description.substring(0, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (descriptionField && data.data.description) {
|
|
|
|
|
|
descriptionField.value = data.data.description;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (logoField && data.data.logo) {
|
|
|
|
|
|
logoField.value = data.data.logo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showStatus('✓ 网站信息获取成功!已自动填充表单', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showStatus('✗ ' + (data.message || '获取失败,请手动填写'), 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('Error:', error);
|
|
|
|
|
|
showStatus('✗ 网络请求失败,请手动填写', 'error');
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
fetchBtn.disabled = false;
|
|
|
|
|
|
fetchBtn.classList.remove('loading');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function showStatus(message, type) {
|
|
|
|
|
|
statusDiv.textContent = message;
|
|
|
|
|
|
statusDiv.className = 'fetch-status ' + type;
|
|
|
|
|
|
statusDiv.style.display = 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
2025-12-30 00:45:39 +08:00
|
|
|
|
// 在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) {
|
|
|
|
|
|
// 先等待一下,确保 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 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生成主要功能';
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
2025-12-30 00:45:39 +08:00
|
|
|
|
const featuresStatusDiv = document.createElement('div');
|
|
|
|
|
|
featuresStatusDiv.className = 'tags-status';
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
2025-12-30 00:45:39 +08:00
|
|
|
|
featuresField.parentNode.appendChild(generateFeaturesBtn);
|
|
|
|
|
|
featuresField.parentNode.appendChild(featuresStatusDiv);
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
2025-12-30 00:45:39 +08:00
|
|
|
|
generateFeaturesBtn.addEventListener('click', function() {
|
2025-12-28 19:21:17 +08:00
|
|
|
|
const nameField = document.querySelector('input[name="name"]');
|
|
|
|
|
|
const descriptionField = document.querySelector('textarea[name="description"]');
|
2025-12-30 00:45:39 +08:00
|
|
|
|
const urlField = document.querySelector('input[name="url"]');
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
|
|
|
|
|
const name = nameField ? nameField.value.trim() : '';
|
|
|
|
|
|
const description = descriptionField ? descriptionField.value.trim() : '';
|
2025-12-30 00:45:39 +08:00
|
|
|
|
const url = urlField ? urlField.value.trim() : '';
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
|
|
|
|
|
if (!name || !description) {
|
2025-12-30 00:45:39 +08:00
|
|
|
|
showFeaturesStatus('请先填写网站名称和描述', 'error');
|
2025-12-28 19:21:17 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示加载状态
|
2025-12-30 00:45:39 +08:00
|
|
|
|
generateFeaturesBtn.disabled = true;
|
|
|
|
|
|
generateFeaturesBtn.classList.add('loading');
|
|
|
|
|
|
featuresStatusDiv.style.display = 'none';
|
2025-12-28 19:21:17 +08:00
|
|
|
|
|
2025-12-30 00:45:39 +08:00
|
|
|
|
// 调用API生成功能
|
|
|
|
|
|
fetch('/api/generate-features', {
|
2025-12-28 19:21:17 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
name: name,
|
2025-12-30 00:45:39 +08:00
|
|
|
|
description: description,
|
|
|
|
|
|
url: url
|
2025-12-28 19:21:17 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(response => response.json())
|
|
|
|
|
|
.then(data => {
|
2025-12-30 00:45:39 +08:00
|
|
|
|
if (data.success && data.features) {
|
|
|
|
|
|
// 自动填充到features字段
|
|
|
|
|
|
featuresField.value = data.features;
|
|
|
|
|
|
showFeaturesStatus('✓ AI已生成主要功能列表', 'success');
|
2025-12-28 19:21:17 +08:00
|
|
|
|
} else {
|
2025-12-30 00:45:39 +08:00
|
|
|
|
showFeaturesStatus('✗ ' + (data.message || '功能生成失败'), 'error');
|
2025-12-28 19:21:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
console.error('Error:', error);
|
2025-12-30 00:45:39 +08:00
|
|
|
|
showFeaturesStatus('✗ 网络请求失败,请重试', 'error');
|
2025-12-28 19:21:17 +08:00
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
2025-12-30 00:45:39 +08:00
|
|
|
|
generateFeaturesBtn.disabled = false;
|
|
|
|
|
|
generateFeaturesBtn.classList.remove('loading');
|
2025-12-28 19:21:17 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-30 00:45:39 +08:00
|
|
|
|
function showFeaturesStatus(message, type) {
|
|
|
|
|
|
featuresStatusDiv.textContent = message;
|
|
|
|
|
|
featuresStatusDiv.className = 'tags-status ' + type;
|
|
|
|
|
|
featuresStatusDiv.style.display = 'block';
|
2025-12-28 19:21:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-27 22:45:09 +08:00
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
{% endblock %}
|