release: v2.1 - Prompt管理系统、页脚优化、图标修复
新增功能: - Prompt管理:后台新增Prompt模板管理功能 - 数据库迁移:新增prompt_templates表及默认数据 - 页脚优化:添加ICP备案号(浙ICP备2025154782号-1)和Microsoft Clarity统计 - 图标修复:详情页Material Icons替换为Emoji - 标签显示:修复编辑页标签名称无法显示的问题 技术改进: - 添加正则表达式提取标签名称 - 优化页脚样式和链接 - 完善增量部署文档 🚀 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 %}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,14 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}ZJPB - 焦提示词 | AI工具导航{% endblock %}</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=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -38,7 +30,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
@@ -139,12 +131,12 @@
|
||||
background: var(--bg-white);
|
||||
}
|
||||
|
||||
.search-box .material-symbols-outlined {
|
||||
.search-box .search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -235,7 +227,7 @@
|
||||
<div class="nav-left">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-icon">
|
||||
<span class="material-symbols-outlined" style="font-size: 20px;">blur_on</span>
|
||||
<span style="font-size: 20px;">✦</span>
|
||||
</div>
|
||||
<span>ZJPB</span>
|
||||
</a>
|
||||
@@ -247,7 +239,7 @@
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<form action="/" method="get" class="search-box">
|
||||
<span class="material-symbols-outlined">search</span>
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="text" name="q" placeholder="搜索 AI 工具..." value="{{ search_query or '' }}">
|
||||
</form>
|
||||
<a href="/admin/login" class="btn btn-secondary">登录</a>
|
||||
@@ -263,16 +255,30 @@
|
||||
<footer class="footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-text">
|
||||
© 2023 ZJPB AI Directory. All rights reserved.
|
||||
<div>© 2025 ZJPB - 焦提示词 | AI工具导航. All rights reserved.</div>
|
||||
<div style="margin-top: 8px;">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" style="color: var(--text-secondary); text-decoration: none;">
|
||||
浙ICP备2025154782号-1
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="#">Twitter</a>
|
||||
<a href="#">Discord</a>
|
||||
<a href="#">Privacy Policy</a>
|
||||
<a href="#">关于我们</a>
|
||||
<a href="#">隐私政策</a>
|
||||
<a href="#">用户协议</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Microsoft Clarity 统计代码 -->
|
||||
<script type="text/javascript">
|
||||
(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "uoa2j40sf0");
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,12 +20,6 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.back-link .material-symbols-outlined {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
/* 产品头部区域 */
|
||||
.product-header-wrapper {
|
||||
@@ -84,9 +78,6 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.product-link .material-symbols-outlined {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
@@ -103,9 +94,6 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-item .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.product-tags-list {
|
||||
display: flex;
|
||||
@@ -195,9 +183,6 @@
|
||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
.visit-btn .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.visit-hint {
|
||||
text-align: center;
|
||||
@@ -242,10 +227,6 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-block h2 .material-symbols-outlined {
|
||||
font-size: 24px;
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.content-block p {
|
||||
color: var(--text-secondary);
|
||||
@@ -399,6 +380,126 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Markdown内容样式 */
|
||||
.markdown-content {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin: 16px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.markdown-content ul li {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.markdown-content ul li:before {
|
||||
content: "▸";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--primary-blue);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown-content ol li {
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.8;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background: #f1f5f9;
|
||||
color: #e11d48;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.markdown-content strong {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown-content em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: var(--primary-blue);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
border-bottom-color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid var(--primary-blue);
|
||||
padding-left: 16px;
|
||||
margin: 16px 0;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 968px) {
|
||||
.product-header-wrapper {
|
||||
@@ -432,7 +533,7 @@
|
||||
|
||||
<!-- 返回链接 -->
|
||||
<a href="/" class="back-link">
|
||||
<span class="material-symbols-outlined">arrow_back</span>
|
||||
<span>←</span>
|
||||
返回首页
|
||||
</a>
|
||||
|
||||
@@ -456,16 +557,16 @@
|
||||
<h1>{{ site.name }}</h1>
|
||||
<a href="{{ site.url }}" target="_blank" class="product-link">
|
||||
{{ site.url }}
|
||||
<span class="material-symbols-outlined">open_in_new</span>
|
||||
<span>↗</span>
|
||||
</a>
|
||||
|
||||
<div class="product-meta">
|
||||
<div class="meta-item">
|
||||
<span class="material-symbols-outlined">visibility</span>
|
||||
<span>👁</span>
|
||||
<span>{{ site.view_count | default(0) }} 次浏览</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="material-symbols-outlined">calendar_today</span>
|
||||
<span>📅</span>
|
||||
<span>添加于 {{ site.created_at.strftime('%Y年%m月%d日') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,7 +589,7 @@
|
||||
</div>
|
||||
<a href="{{ site.url }}" target="_blank" class="visit-btn">
|
||||
访问网站
|
||||
<span class="material-symbols-outlined">north_east</span>
|
||||
<span>↗</span>
|
||||
</a>
|
||||
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
|
||||
</div>
|
||||
@@ -501,20 +602,20 @@
|
||||
<!-- Product Overview -->
|
||||
<div class="content-block">
|
||||
<h2>
|
||||
<span class="material-symbols-outlined">info</span>
|
||||
<span>ℹ️</span>
|
||||
产品概述
|
||||
</h2>
|
||||
<p>{{ site.description }}</p>
|
||||
<div class="markdown-content">{{ site.description | markdown | safe }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Description -->
|
||||
{% if site.features %}
|
||||
<div class="content-block">
|
||||
<h2>
|
||||
<span class="material-symbols-outlined">description</span>
|
||||
详细描述
|
||||
<span>📋</span>
|
||||
主要功能
|
||||
</h2>
|
||||
<div>{{ site.features | safe }}</div>
|
||||
<div class="markdown-content">{{ site.features | markdown | safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -522,7 +623,7 @@
|
||||
{% if news_list %}
|
||||
<div class="content-block">
|
||||
<h2>
|
||||
<span class="material-symbols-outlined">newspaper</span>
|
||||
<span>📰</span>
|
||||
相关新闻
|
||||
</h2>
|
||||
{% for news in news_list %}
|
||||
@@ -540,7 +641,7 @@
|
||||
{% if recommended_sites %}
|
||||
<div class="content-block">
|
||||
<h2>
|
||||
<span class="material-symbols-outlined">auto_awesome</span>
|
||||
<span>✨</span>
|
||||
相似推荐
|
||||
</h2>
|
||||
<div class="recommendations-grid">
|
||||
@@ -560,7 +661,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined arrow-icon">north_east</span>
|
||||
<span class="arrow-icon">↗</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user