Files
zjpb.net/templates/admin/site/create.html
Jowe edbc4288f9 feat: v3.0.1 - 后台获取新闻功能优化
1. 新增后台管理获取新闻API (/api/admin/fetch-news/<code>)
   - 不需要验证码,仅管理员可用
   - 用于后台手动触发新闻获取

2. 后台网站管理添加"手动获取新闻"按钮
   - 在新闻关键词字段旁边显示
   - 编辑模式可用

3. 取消前台detail页面手动获取新闻入口
   - 用户不再需要手动刷新新闻
   - 新闻由管理员在后台控制获取

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 22:16:12 +08:00

799 lines
30 KiB
HTML
Raw 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.
{% extends 'admin/model/create.html' %}
{% block tail %}
{{ super() }}
<style>
.auto-fetch-btn {
margin-top: 10px;
margin-bottom: 15px;
}
.generate-tags-btn {
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;
border-radius: 8px;
display: none;
}
.tags-status {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
display: none;
}
.fetch-status.success, .tags-status.success {
background-color: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #4ade80;
}
.fetch-status.error, .tags-status.error {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #f87171;
}
.fetch-news-btn {
margin-top: 10px;
margin-bottom: 15px;
background-color: #e6a23c;
color: white;
}
.fetch-news-btn:hover {
background-color: #cf9236;
}
.news-status {
margin-top: 10px;
padding: 10px;
border-radius: 8px;
display: none;
}
.news-status.success {
background-color: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #4ade80;
}
.news-status.error {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #f87171;
}
.auto-fetch-btn .loading-icon, .generate-tags-btn .loading-icon {
display: none;
}
.auto-fetch-btn.loading .loading-icon, .generate-tags-btn.loading .loading-icon {
display: inline-block;
animation: spin 1s linear infinite;
}
.auto-fetch-btn.loading .normal-icon, .generate-tags-btn.loading .normal-icon {
display: none;
}
@keyframes spin {
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>
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';
}
}
// 在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';
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';
}
}
// 在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';
}
}
// 在 News Keywords 字段后添加"获取新闻"按钮
const newsKeywordsField = document.querySelector('input[name="news_keywords"]');
if (newsKeywordsField) {
// 创建获取新闻按钮
const fetchNewsBtn = document.createElement('button');
fetchNewsBtn.type = 'button';
fetchNewsBtn.className = 'btn fetch-news-btn';
fetchNewsBtn.innerHTML = '<span class="normal-icon">📰</span><span class="loading-icon">↻</span> 手动获取新闻';
const newsStatusDiv = document.createElement('div');
newsStatusDiv.className = 'news-status';
newsKeywordsField.parentNode.appendChild(fetchNewsBtn);
newsKeywordsField.parentNode.appendChild(newsStatusDiv);
// 获取当前网站code创建时没有code需要保存后才有
// 这里使用一个提示
fetchNewsBtn.addEventListener('click', function() {
showNewsStatus('请先保存网站基本信息,保存后再点击此按钮获取新闻', 'error');
});
function showNewsStatus(message, type) {
newsStatusDiv.textContent = message;
newsStatusDiv.className = 'news-status ' + type;
newsStatusDiv.style.display = 'block';
}
}
// 页面加载后如果有已保存的网站code显示获取新闻按钮
// 从URL中提取site_id来判断是否是编辑模式
const urlParams = new URLSearchParams(window.location.search);
const view = urlParams.get('view');
const siteId = urlParams.get('id');
// 如果是编辑模式,添加获取新闻功能
if (siteId) {
const newsKeywordsFieldEdit = document.querySelector('input[name="news_keywords"]');
const existingBtn = document.querySelector('.fetch-news-btn');
if (newsKeywordsFieldEdit && !existingBtn) {
// 创建获取新闻按钮(编辑模式)
const fetchNewsBtn = document.createElement('button');
fetchNewsBtn.type = 'button';
fetchNewsBtn.className = 'btn fetch-news-btn';
fetchNewsBtn.innerHTML = '<span class="normal-icon">📰</span><span class="loading-icon">↻</span> 手动获取新闻';
const newsStatusDiv = document.createElement('div');
newsStatusDiv.className = 'news-status';
newsKeywordsFieldEdit.parentNode.appendChild(fetchNewsBtn);
newsKeywordsFieldEdit.parentNode.appendChild(newsStatusDiv);
// 获取网站code
let siteCode = '';
const codeField = document.querySelector('input[name="code"]');
if (codeField) {
siteCode = codeField.value;
}
fetchNewsBtn.addEventListener('click', function() {
if (!siteCode) {
// 尝试从其他方式获取code
const urlMatch = window.location.pathname.match(/\/admin\/site\/edit\/(\d+)/);
if (urlMatch) {
// 通过网站ID获取code需要API支持这里先提示
showNewsStatus('正在获取网站信息...', 'success');
// 调用API获取网站信息
fetch('/admin/api/site/' + urlMatch[1], {
credentials: 'same-origin'
})
.then(response => response.json())
.then(data => {
if (data.code) {
fetchNews(siteCode);
}
})
.catch(() => {
showNewsStatus('无法获取网站编码,请确保网站已保存', 'error');
});
} else {
showNewsStatus('请先保存网站', 'error');
}
return;
}
fetchNews(siteCode);
});
function fetchNews(code) {
// 显示加载状态
fetchNewsBtn.disabled = true;
fetchNewsBtn.classList.add('loading');
fetchNewsBtn.innerHTML = '<span class="loading-icon">↻</span> 获取中...';
newsStatusDiv.style.display = 'none';
// 调用后台获取新闻API
fetch('/api/admin/fetch-news/' + code, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNewsStatus('✓ ' + data.message, 'success');
} else {
showNewsStatus('✗ ' + (data.error || '获取失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNewsStatus('✗ 网络请求失败', 'error');
})
.finally(() => {
fetchNewsBtn.disabled = false;
fetchNewsBtn.classList.remove('loading');
fetchNewsBtn.innerHTML = '<span class="normal-icon">📰</span><span class="loading-icon">↻</span> 手动获取新闻';
});
}
function showNewsStatus(message, type) {
newsStatusDiv.textContent = message;
newsStatusDiv.className = 'news-status ' + type;
newsStatusDiv.style.display = 'block';
}
}
}
});
</script>
{% endblock %}