Files
zjpb.net/v2.1.0.patch

2439 lines
89 KiB
Diff
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
From 30b1ef75d655f962da385ad8630e06562a8b5bdf Mon Sep 17 00:00:00 2001
From: Jowe <123822645+Selei1983@users.noreply.github.com>
Date: Tue, 30 Dec 2025 00:45:39 +0800
Subject: [PATCH] =?UTF-8?q?release:=20v2.1=20-=20Prompt=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E7=B3=BB=E7=BB=9F=E3=80=81=E9=A1=B5=E8=84=9A=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E3=80=81=E5=9B=BE=E6=A0=87=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增功能:
- 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>
---
INCREMENTAL_DEPLOY.md | 208 ++++++++++++
app.py | 313 +++++++++++++++++-
export_data.py | 58 ++++
migrate_db.py | 49 +++
migrate_prompts.py | 141 +++++++++
models.py | 32 ++
templates/admin/site/create.html | 455 ++++++++++++++++++++++++++-
templates/admin/site/edit.html | 522 +++++++++++++++++++++++++++++--
templates/base_new.html | 40 ++-
templates/detail_new.html | 165 ++++++++--
10 files changed, 1887 insertions(+), 96 deletions(-)
create mode 100644 INCREMENTAL_DEPLOY.md
create mode 100644 export_data.py
create mode 100644 migrate_db.py
create mode 100644 migrate_prompts.py
diff --git a/INCREMENTAL_DEPLOY.md b/INCREMENTAL_DEPLOY.md
new file mode 100644
index 0000000..dc18d04
--- /dev/null
+++ b/INCREMENTAL_DEPLOY.md
@@ -0,0 +1,208 @@
+# ZJPB v2.1 增量部署指南
+
+## 📋 本次更新内容
+
+### 新增功能
+1. **Prompt管理系统** - 后台可管理AI提示词模板
+2. **详情页图标优化** - Material Icons替换为Emoji
+3. **标签显示修复** - 修复编辑页标签名称无法显示问题
+4. **页脚优化** - 添加ICP备案号和Microsoft Clarity统计
+
+### 数据库变更
+- 新增表:`prompt_templates` Prompt模板表
+- 无现有表结构变更,完全兼容旧数据
+
+---
+
+## 🔒 增量部署步骤
+
+### 第一步:备份生产数据库(必须!)
+
+在1Panel中备份数据库
+```bash
+# 方法1使用1Panel面板
+1. 进入1Panel -> 数据库
+2. 找到 ai_nav 数据库
+3. 点击"备份"按钮
+4. 下载备份文件到本地保存
+
+# 方法2使用命令行
+mysqldump -h 112.124.42.38 -u ai_nav_user -p ai_nav > backup_$(date +%Y%m%d_%H%M%S).sql
+```
+
+### 第二步:停止生产应用
+
+```bash
+# SSH登录到生产服务器
+ssh root@your-server-ip
+
+# 进入应用目录
+cd /www/wwwroot/zjpb
+
+# 停止应用
+./manage.sh stop
+```
+
+### 第三步:备份现有代码
+
+```bash
+# 创建备份目录
+cd /www/wwwroot
+cp -r zjpb zjpb_backup_$(date +%Y%m%d_%H%M%S)
+```
+
+### 第四步:上传新代码
+
+**方法1使用Git推荐**
+
+```bash
+# 在本地提交所有修改
+git add .
+git commit -m "release: v2.1 - Prompt管理系统、页脚优化、图标修复"
+git push origin master
+
+# 在服务器上拉取
+cd /www/wwwroot/zjpb
+git pull origin master
+```
+
+**方法2手动上传**
+
+```bash
+# 在本地压缩(排除不需要的文件)
+zip -r zjpb_v2.1.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py" "*.db" "nul"
+
+# 上传到服务器 /www/wwwroot/zjpb_new.zip
+# 然后解压覆盖
+cd /www/wwwroot/zjpb
+unzip -o ../zjpb_new.zip
+```
+
+### 第五步:安装新依赖(如有)
+
+```bash
+cd /www/wwwroot/zjpb
+source venv/bin/activate
+pip install -r requirements.txt
+```
+
+### 第六步:运行数据库迁移
+
+```bash
+# 激活虚拟环境
+source venv/bin/activate
+
+# 运行迁移脚本(创建 prompt_templates 表)
+python migrate_prompts.py
+```
+
+**预期输出:**
+```
+正在创建 prompt_templates 表...
+[OK] 表创建成功
+正在初始化默认prompt模板...
+[OK] 默认prompt模板初始化成功
+ - 标签生成: 1
+ - 主要功能生成: 2
+ - 详细介绍生成: 3
+```
+
+### 第七步:重启应用
+
+```bash
+./manage.sh start
+```
+
+### 第八步:验证部署
+
+**检查项:**
+
+1. **访问前台首页**
+ - 检查页脚是否显示ICP备案号
+ - 检查Clarity统计是否加载F12查看Network
+
+2. **访问详情页**
+ - 检查图标是否正常显示不是Material Icons文本
+
+3. **登录后台**
+ - 检查是否有"Prompt管理"菜单
+ - 进入Prompt管理查看是否有3条默认数据
+
+4. **测试网站编辑**
+ - 编辑任意网站,检查标签是否正常显示(不是空白蓝框)
+
+5. **测试AI功能**
+ - 创建新网站,测试"AI生成标签"功能
+ - 测试"AI生成详细介绍"功能
+ - 测试"AI生成主要功能"功能
+
+---
+
+## 🔄 回滚方案(如出现问题)
+
+### 快速回滚代码
+
+```bash
+cd /www/wwwroot
+./zjpb/manage.sh stop
+
+# 删除新版本
+rm -rf zjpb
+
+# 恢复备份
+mv zjpb_backup_YYYYMMDD_HHMMSS zjpb
+
+# 重启
+cd zjpb
+./manage.sh start
+```
+
+### 恢复数据库
+
+```bash
+# 如果新表导致问题,可以删除新表
+mysql -h 112.124.42.38 -u ai_nav_user -p ai_nav
+
+# 在MySQL中执行
+DROP TABLE IF EXISTS prompt_templates;
+```
+
+---
+
+## 📝 注意事项
+
+1. ✅ **本次更新不会影响现有数据** - 只是新增表,不修改现有表
+2. ✅ **完全向后兼容** - 即使不运行迁移脚本,前台也能正常访问
+3. ✅ **可以随时回滚** - 保留了完整备份
+4. ⚠️ **必须备份数据库** - 虽然风险很低,但备份是必须的
+5. ⚠️ **检查.env配置** - 确保生产环境的.env配置正确
+
+---
+
+## 🐛 常见问题
+
+### Q1: 迁移脚本报错 "表已存在"
+**A:** 说明之前已经运行过迁移,可以跳过此步骤
+
+### Q2: Prompt管理菜单看不到
+**A:** 清除浏览器缓存,重新登录后台
+
+### Q3: 标签还是显示不出来
+**A:** 清除浏览器缓存强制刷新Ctrl+F5
+
+### Q4: Clarity统计没加载
+**A:** 检查网络是否能访问 clarity.ms可能被墙
+
+---
+
+## 📞 技术支持
+
+如遇问题,请检查日志:
+
+```bash
+# 查看应用日志
+./manage.sh logs
+
+# 查看错误日志
+tail -f logs/error.log
+```
diff --git a/app.py b/app.py
index 14288f1..7a2e392 100644
--- a/app.py
+++ b/app.py
@@ -1,11 +1,12 @@
import os
+import markdown
from flask import Flask, render_template, redirect, url_for, request, flash, jsonify
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_admin import Admin, AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView
from datetime import datetime
from config import config
-from models import db, Site, Tag, Admin as AdminModel, News
+from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate
from utils.website_fetcher import WebsiteFetcher
from utils.tag_generator import TagGenerator
@@ -19,6 +20,14 @@ def create_app(config_name='default'):
# 初始化数据库
db.init_app(app)
+ # 添加Markdown过滤器
+ @app.template_filter('markdown')
+ def markdown_filter(text):
+ """将Markdown文本转换为HTML"""
+ if not text:
+ return ''
+ return markdown.markdown(text, extensions=['nl2br', 'fenced_code'])
+
# 初始化登录管理
login_manager = LoginManager()
login_manager.init_app(app)
@@ -36,6 +45,22 @@ def create_app(config_name='default'):
# 获取所有启用的标签
tags = Tag.query.order_by(Tag.sort_order.desc(), Tag.id).all()
+ # 优化使用一次SQL查询统计所有标签的网站数量
+ tag_counts = {}
+ if tags:
+ # 使用JOIN查询一次性获取所有标签的网站数量
+ from sqlalchemy import func
+ counts_query = db.session.query(
+ site_tags.c.tag_id,
+ func.count(site_tags.c.site_id).label('count')
+ ).join(
+ Site, site_tags.c.site_id == Site.id
+ ).filter(
+ Site.is_active == True
+ ).group_by(site_tags.c.tag_id).all()
+
+ tag_counts = {tag_id: count for tag_id, count in counts_query}
+
# 获取筛选参数
tag_slug = request.args.get('tag')
search_query = request.args.get('q', '').strip()
@@ -57,7 +82,7 @@ def create_app(config_name='default'):
pagination = None
return render_template('index_new.html', sites=sites, tags=tags,
selected_tag=selected_tag, search_query=search_query,
- pagination=pagination)
+ pagination=pagination, tag_counts=tag_counts)
# 搜索功能
if search_query:
@@ -79,7 +104,7 @@ def create_app(config_name='default'):
return render_template('index_new.html', sites=sites, tags=tags,
selected_tag=selected_tag, search_query=search_query,
- pagination=pagination)
+ pagination=pagination, tag_counts=tag_counts)
@app.route('/site/<code>')
def site_detail(code):
@@ -138,6 +163,51 @@ def create_app(config_name='default'):
logout_user()
return redirect(url_for('index'))
+ @app.route('/admin/change-password', methods=['GET', 'POST'])
+ @login_required
+ def change_password():
+ """修改密码"""
+ if request.method == 'POST':
+ old_password = request.form.get('old_password', '').strip()
+ new_password = request.form.get('new_password', '').strip()
+ confirm_password = request.form.get('confirm_password', '').strip()
+
+ # 验证旧密码
+ if not current_user.check_password(old_password):
+ flash('旧密码错误', 'error')
+ return render_template('admin/change_password.html')
+
+ # 验证新密码
+ if not new_password:
+ flash('新密码不能为空', 'error')
+ return render_template('admin/change_password.html')
+
+ if len(new_password) < 6:
+ flash('新密码长度至少6位', 'error')
+ return render_template('admin/change_password.html')
+
+ if new_password != confirm_password:
+ flash('两次输入的新密码不一致', 'error')
+ return render_template('admin/change_password.html')
+
+ if old_password == new_password:
+ flash('新密码不能与旧密码相同', 'error')
+ return render_template('admin/change_password.html')
+
+ # 修改密码
+ try:
+ current_user.set_password(new_password)
+ db.session.commit()
+ flash('密码修改成功,请重新登录', 'success')
+ logout_user()
+ return redirect(url_for('admin_login'))
+ except Exception as e:
+ db.session.rollback()
+ flash(f'密码修改失败:{str(e)}', 'error')
+ return render_template('admin/change_password.html')
+
+ return render_template('admin/change_password.html')
+
# ========== API路由 ==========
@app.route('/api/fetch-website-info', methods=['POST'])
@login_required
@@ -165,17 +235,18 @@ def create_app(config_name='default'):
'message': '无法获取网站信息请检查URL是否正确或手动填写'
})
- # 下载Logo如果有
+ # 下载Logo到本地如果有
logo_path = None
if info.get('logo_url'):
- logo_path = fetcher.download_logo(info['logo_url'])
+ logo_path = fetcher.download_logo(info['logo_url'], save_dir='static/logos')
+ # 如果下载失败不返回远程URL让用户手动上传
return jsonify({
'success': True,
'data': {
'name': info.get('name', ''),
'description': info.get('description', ''),
- 'logo': logo_path or info.get('logo_url', '')
+ 'logo': logo_path if logo_path else ''
}
})
@@ -185,6 +256,148 @@ def create_app(config_name='default'):
'message': f'抓取失败: {str(e)}'
}), 500
+ @app.route('/api/upload-logo', methods=['POST'])
+ @login_required
+ def upload_logo():
+ """上传Logo图片API"""
+ try:
+ # 检查文件是否存在
+ if 'logo' not in request.files:
+ return jsonify({
+ 'success': False,
+ 'message': '请选择要上传的图片'
+ }), 400
+
+ file = request.files['logo']
+
+ # 检查文件名
+ if file.filename == '':
+ return jsonify({
+ 'success': False,
+ 'message': '未选择文件'
+ }), 400
+
+ # 检查文件类型
+ allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp'}
+ filename = file.filename.lower()
+ if not any(filename.endswith('.' + ext) for ext in allowed_extensions):
+ return jsonify({
+ 'success': False,
+ 'message': '不支持的文件格式,请上传图片文件(png, jpg, gif, svg, ico, webp)'
+ }), 400
+
+ # 创建保存目录
+ save_dir = 'static/logos'
+ os.makedirs(save_dir, exist_ok=True)
+
+ # 生成安全的文件名
+ import time
+ import hashlib
+ ext = os.path.splitext(filename)[1]
+ timestamp = str(int(time.time() * 1000))
+ hash_name = hashlib.md5(f"{filename}{timestamp}".encode()).hexdigest()[:16]
+ safe_filename = f"logo_{hash_name}{ext}"
+ filepath = os.path.join(save_dir, safe_filename)
+
+ # 保存文件
+ file.save(filepath)
+
+ # 返回相对路径
+ return jsonify({
+ 'success': True,
+ 'path': f'/{filepath.replace(os.sep, "/")}'
+ })
+
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'上传失败: {str(e)}'
+ }), 500
+
+ @app.route('/api/generate-features', methods=['POST'])
+ @login_required
+ def generate_features():
+ """使用DeepSeek自动生成网站主要功能"""
+ try:
+ data = request.get_json()
+ name = data.get('name', '').strip()
+ description = data.get('description', '').strip()
+ url = data.get('url', '').strip()
+
+ if not name or not description:
+ return jsonify({
+ 'success': False,
+ 'message': '请提供网站名称和描述'
+ }), 400
+
+ # 生成功能列表
+ generator = TagGenerator()
+ features = generator.generate_features(name, description, url)
+
+ if not features:
+ return jsonify({
+ 'success': False,
+ 'message': 'DeepSeek功能生成失败请检查API配置'
+ }), 500
+
+ return jsonify({
+ 'success': True,
+ 'features': features
+ })
+
+ except ValueError as e:
+ return jsonify({
+ 'success': False,
+ 'message': str(e)
+ }), 400
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'生成失败: {str(e)}'
+ }), 500
+
+ @app.route('/api/generate-description', methods=['POST'])
+ @login_required
+ def generate_description():
+ """使用DeepSeek自动生成网站详细介绍"""
+ try:
+ data = request.get_json()
+ name = data.get('name', '').strip()
+ short_desc = data.get('short_desc', '').strip()
+ url = data.get('url', '').strip()
+
+ if not name:
+ return jsonify({
+ 'success': False,
+ 'message': '请提供网站名称'
+ }), 400
+
+ # 生成详细介绍
+ generator = TagGenerator()
+ description = generator.generate_description(name, short_desc, url)
+
+ if not description:
+ return jsonify({
+ 'success': False,
+ 'message': 'DeepSeek详细介绍生成失败请检查API配置'
+ }), 500
+
+ return jsonify({
+ 'success': True,
+ 'description': description
+ })
+
+ except ValueError as e:
+ return jsonify({
+ 'success': False,
+ 'message': str(e)
+ }), 400
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'message': f'生成失败: {str(e)}'
+ }), 500
+
@app.route('/api/generate-tags', methods=['POST'])
@login_required
def generate_tags():
@@ -345,11 +558,11 @@ def create_app(config_name='default'):
})
continue
- # 4. 下载Logo失败不影响导入
+ # 4. 下载Logo到本地失败不影响导入
logo_path = None
if info.get('logo_url'):
try:
- logo_path = fetcher.download_logo(info['logo_url'])
+ logo_path = fetcher.download_logo(info['logo_url'], save_dir='static/logos')
except Exception as e:
print(f"下载Logo失败 ({url}): {str(e)}")
# Logo下载失败不影响网站导入
@@ -544,9 +757,47 @@ def create_app(config_name='default'):
import re
import random
from pypinyin import lazy_pinyin
+ from flask import request
# 使用no_autoflush防止在查询时触发提前flush
with db.session.no_autoflush:
+ # 处理手动输入的新标签
+ new_tags_str = request.form.get('new_tags', '')
+ if new_tags_str:
+ new_tag_names = [name.strip() for name in new_tags_str.split(',') if name.strip()]
+ for tag_name in new_tag_names:
+ # 检查标签是否已存在
+ existing_tag = Tag.query.filter_by(name=tag_name).first()
+ if not existing_tag:
+ # 创建新标签
+ tag_slug = ''.join(lazy_pinyin(tag_name))
+ tag_slug = tag_slug.lower()
+ tag_slug = re.sub(r'[^\w\s-]', '', tag_slug)
+ tag_slug = re.sub(r'[-\s]+', '-', tag_slug).strip('-')
+
+ # 确保slug唯一
+ base_tag_slug = tag_slug[:50]
+ counter = 1
+ final_tag_slug = tag_slug
+ while Tag.query.filter_by(slug=final_tag_slug).first():
+ final_tag_slug = f"{base_tag_slug}-{counter}"
+ counter += 1
+ if counter > 100:
+ final_tag_slug = f"{base_tag_slug}-{random.randint(1000, 9999)}"
+ break
+
+ new_tag = Tag(name=tag_name, slug=final_tag_slug)
+ db.session.add(new_tag)
+ db.session.flush() # 确保新标签有ID
+
+ # 添加到模型的标签列表
+ if new_tag not in model.tags:
+ model.tags.append(new_tag)
+ else:
+ # 添加已存在的标签
+ if existing_tag not in model.tags:
+ model.tags.append(existing_tag)
+
# 如果code为空自动生成唯一的8位数字编码
if not model.code or model.code.strip() == '':
max_attempts = 10
@@ -684,6 +935,51 @@ def create_app(config_name='default'):
]
}
+ # Prompt模板管理视图
+ class PromptAdmin(SecureModelView):
+ can_edit = True
+ can_delete = False # 不允许删除避免系统必需的prompt被删除
+ can_create = True
+ can_view_details = False
+
+ # 显示操作列
+ column_display_actions = True
+
+ column_list = ['id', 'key', 'name', 'description', 'is_active', 'updated_at']
+ column_searchable_list = ['key', 'name', 'description']
+ column_filters = ['is_active', 'key']
+ column_labels = {
+ 'id': 'ID',
+ 'key': '唯一标识',
+ 'name': '模板名称',
+ 'system_prompt': '系统提示词',
+ 'user_prompt_template': '用户提示词模板',
+ 'description': '模板说明',
+ 'is_active': '是否启用',
+ 'created_at': '创建时间',
+ 'updated_at': '更新时间'
+ }
+ form_columns = ['key', 'name', 'description', 'system_prompt', 'user_prompt_template', 'is_active']
+
+ # 字段说明
+ column_descriptions = {
+ 'key': '唯一标识,如: tags, features, description',
+ 'system_prompt': 'AI的系统角色设定',
+ 'user_prompt_template': '用户提示词模板,支持变量如 {name}, {description}, {url}',
+ }
+
+ # 表单字段配置
+ form_widget_args = {
+ 'system_prompt': {
+ 'rows': 3,
+ 'style': 'font-family: monospace;'
+ },
+ 'user_prompt_template': {
+ 'rows': 20,
+ 'style': 'font-family: monospace;'
+ }
+ }
+
# 初始化 Flask-Admin
admin = Admin(
app,
@@ -696,6 +992,7 @@ def create_app(config_name='default'):
admin.add_view(SiteAdmin(Site, db.session, name='网站管理'))
admin.add_view(TagAdmin(Tag, db.session, name='标签管理'))
admin.add_view(NewsAdmin(News, db.session, name='新闻管理'))
+ admin.add_view(PromptAdmin(PromptTemplate, db.session, name='Prompt管理'))
admin.add_view(AdminAdmin(AdminModel, db.session, name='管理员', endpoint='admin_users'))
return app
diff --git a/export_data.py b/export_data.py
new file mode 100644
index 0000000..df212a9
--- /dev/null
+++ b/export_data.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+"""
+数据导出脚本
+导出现有数据到SQL文件
+"""
+import subprocess
+import os
+from datetime import datetime
+
+def export_database():
+ """导出数据库到SQL文件"""
+ # 从.env读取数据库配置
+ from dotenv import load_dotenv
+ load_dotenv()
+
+ db_host = os.getenv('DB_HOST', 'localhost')
+ db_port = os.getenv('DB_PORT', '3306')
+ db_user = os.getenv('DB_USER')
+ db_password = os.getenv('DB_PASSWORD')
+ db_name = os.getenv('DB_NAME')
+
+ # 生成备份文件名
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ backup_file = f'backup_{db_name}_{timestamp}.sql'
+
+ # mysqldump命令
+ cmd = [
+ 'mysqldump',
+ '-h', db_host,
+ '-P', db_port,
+ '-u', db_user,
+ f'-p{db_password}',
+ '--single-transaction',
+ '--routines',
+ '--triggers',
+ db_name
+ ]
+
+ print(f"正在导出数据库 {db_name} ...")
+
+ try:
+ with open(backup_file, 'w', encoding='utf-8') as f:
+ subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, check=True)
+
+ file_size = os.path.getsize(backup_file) / 1024 # KB
+ print(f"✓ 数据库导出成功!")
+ print(f" 文件: {backup_file}")
+ print(f" 大小: {file_size:.2f} KB")
+ print(f"\n请将此文件上传到服务器进行恢复")
+
+ except subprocess.CalledProcessError as e:
+ print(f"✗ 导出失败: {e.stderr.decode()}")
+ except FileNotFoundError:
+ print("✗ 错误: 找不到 mysqldump 命令")
+ print(" 请确保MySQL客户端工具已安装并在PATH中")
+
+if __name__ == '__main__':
+ export_database()
diff --git a/migrate_db.py b/migrate_db.py
new file mode 100644
index 0000000..4d4bdef
--- /dev/null
+++ b/migrate_db.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""
+数据库安全迁移脚本
+只创建缺失的表,不删除现有数据
+用于生产环境部署
+"""
+
+import sys
+from app import create_app
+from models import db, Admin
+
+def migrate_database():
+ """安全迁移数据库(不删除数据)"""
+ app = create_app('production')
+
+ with app.app_context():
+ print("正在检查数据库表...")
+
+ # 只创建不存在的表(不会删除数据)
+ db.create_all()
+ print("✓ 数据库表检查完成")
+
+ # 检查是否存在管理员
+ admin_count = Admin.query.count()
+
+ if admin_count == 0:
+ print("\n未找到管理员账号正在创建默认管理员...")
+ admin = Admin(
+ username='admin',
+ email='admin@example.com',
+ is_active=True
+ )
+ admin.set_password('admin123')
+ db.session.add(admin)
+ db.session.commit()
+
+ print("✓ 默认管理员创建成功")
+ print(" 用户名: admin")
+ print(" 密码: admin123")
+ print("\n⚠ 请立即登录后台修改密码!")
+ else:
+ print(f"\n✓ 已存在 {admin_count} 个管理员账号")
+
+ print("\n" + "="*50)
+ print("数据库迁移完成!")
+ print("="*50)
+
+if __name__ == '__main__':
+ migrate_database()
diff --git a/migrate_prompts.py b/migrate_prompts.py
new file mode 100644
index 0000000..a6bd693
--- /dev/null
+++ b/migrate_prompts.py
@@ -0,0 +1,141 @@
+"""创建 prompt_templates 表并初始化默认数据"""
+import os
+import sys
+from app import create_app
+from models import db, PromptTemplate
+
+# 设置输出编码为UTF-8
+if sys.platform.startswith('win'):
+ sys.stdout.reconfigure(encoding='utf-8')
+
+def migrate_prompts():
+ """创建表并初始化默认prompt模板"""
+ app = create_app(os.getenv('FLASK_ENV', 'development'))
+
+ with app.app_context():
+ # 创建表
+ print("正在创建 prompt_templates 表...")
+ db.create_all()
+ print("[OK] 表创建成功")
+
+ # 检查是否已有数据
+ existing_count = PromptTemplate.query.count()
+ if existing_count > 0:
+ print(f"[WARN] 表中已有 {existing_count} 条数据,跳过初始化")
+ return
+
+ # 初始化默认prompt模板
+ print("正在初始化默认prompt模板...")
+
+ # 1. 标签生成模板
+ tags_prompt = PromptTemplate(
+ key='tags',
+ name='标签生成',
+ description='根据网站名称和描述生成3-5个分类标签',
+ system_prompt='你是一个专业的AI工具分类专家擅长为各类AI产品生成准确的标签。',
+ user_prompt_template='''你是一个AI工具导航网站的标签生成助手。根据以下产品信息生成3-5个最合适的标签。
+
+产品名称: {name}
+
+产品描述: {description}
+{existing_tags}
+
+要求:
+1. 标签应该准确描述产品的功能、类型或应用场景
+2. 每个标签2-4个汉字
+3. 标签要具体且有区分度
+4. 如果是AI工具可以标注具体的AI类型如"GPT"、"图像生成"等)
+5. 只返回标签,用逗号分隔,不要其他说明
+
+示例输出格式:写作助手,营销,GPT,内容生成
+
+请生成标签:''',
+ is_active=True
+ )
+
+ # 2. 主要功能生成模板
+ features_prompt = PromptTemplate(
+ key='features',
+ name='主要功能生成',
+ description='根据网站名称和描述生成5-8个主要功能点',
+ system_prompt='你是一个专业的AI产品文案专家擅长提炼产品核心功能和价值点。',
+ user_prompt_template='''你是一个AI工具导航网站的内容编辑助手。根据以下产品信息生成详细的主要功能列表。
+
+产品名称: {name}
+
+产品描述: {description}
+{url_info}
+
+要求:
+1. 生成5-8个主要功能点
+2. 每个功能点要具体、清晰、有吸引力
+3. 使用Markdown无序列表格式以"- "开头)
+4. 每个功能点一行简洁有力10-30字
+5. 突出产品的核心价值和特色功能
+6. 使用专业但易懂的语言
+7. 不要添加任何标题或额外说明,直接输出功能列表
+
+示例输出格式:
+- 智能文本生成,支持多种写作场景
+- 实时语法检查和优化建议
+- 多语言翻译准确率高达95%
+- 一键生成营销文案和广告语
+- 团队协作,支持多人同时编辑
+
+请生成功能列表:''',
+ is_active=True
+ )
+
+ # 3. 详细介绍生成模板
+ description_prompt = PromptTemplate(
+ key='description',
+ name='详细介绍生成',
+ description='根据网站名称和简短描述生成200-400字的详细介绍',
+ system_prompt='你是一个专业的AI产品文案专家擅长撰写准确、客观、有吸引力的产品介绍。',
+ user_prompt_template='''你是一个AI工具导航网站的内容编辑助手。根据以下产品信息生成详细、专业且吸引人的产品介绍。
+
+产品名称: {name}
+{short_desc_info}
+{url_info}
+
+要求:
+1. 生成200-400字的详细介绍
+2. 包含以下内容:
+ - 产品定位和核心价值(这是什么产品,解决什么问题)
+ - 主要特点和优势(为什么选择这个产品)
+ - 适用场景和目标用户(谁会用,用在哪里)
+3. 使用Markdown格式可以包含
+ - 段落分隔(空行)
+ - 加粗重点内容(**文字**
+ - 列表(- 列表项)
+4. 语言专业但易懂,突出产品价值
+5. 不要添加标题,直接输出正文内容
+6. 语气客观、事实性强,避免过度营销
+
+示例输出格式:
+ChatGPT是由OpenAI开发的**先进对话式AI助手**基于GPT-4大语言模型构建。它能够理解和生成自然语言为用户提供智能对话、内容创作、代码编写等多种服务。
+
+**核心优势:**
+- 强大的语言理解和生成能力
+- 支持多轮对话,上下文连贯
+- 覆盖编程、写作、翻译等多个领域
+
+适用于内容创作者、程序员、学生等各类用户,可用于日常问答、文案撰写、学习辅导、编程助手等多种场景。
+
+请生成详细介绍:''',
+ is_active=True
+ )
+
+ # 添加到数据库
+ db.session.add(tags_prompt)
+ db.session.add(features_prompt)
+ db.session.add(description_prompt)
+ db.session.commit()
+
+ print("[OK] 默认prompt模板初始化成功")
+ print(f" - 标签生成: {tags_prompt.id}")
+ print(f" - 主要功能生成: {features_prompt.id}")
+ print(f" - 详细介绍生成: {description_prompt.id}")
+
+if __name__ == '__main__':
+ migrate_prompts()
diff --git a/models.py b/models.py
index 57e988a..fae1c97 100644
--- a/models.py
+++ b/models.py
@@ -136,3 +136,35 @@ class Admin(UserMixin, db.Model):
def __repr__(self):
return f'<Admin {self.username}>'
+
+class PromptTemplate(db.Model):
+ """AI提示词模板模型"""
+ __tablename__ = 'prompt_templates'
+
+ id = db.Column(db.Integer, primary_key=True)
+ key = db.Column(db.String(50), unique=True, nullable=False, comment='唯一标识(tags/features/description)')
+ name = db.Column(db.String(100), nullable=False, comment='模板名称')
+ system_prompt = db.Column(db.Text, nullable=False, comment='系统提示词')
+ user_prompt_template = db.Column(db.Text, nullable=False, comment='用户提示词模板(支持变量)')
+ description = db.Column(db.String(200), comment='模板说明')
+ is_active = db.Column(db.Boolean, default=True, comment='是否启用')
+ created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
+ updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
+
+ def __repr__(self):
+ return f'<PromptTemplate {self.name}>'
+
+ def to_dict(self):
+ """转换为字典"""
+ return {
+ 'id': self.id,
+ 'key': self.key,
+ 'name': self.name,
+ 'system_prompt': self.system_prompt,
+ 'user_prompt_template': self.user_prompt_template,
+ 'description': self.description,
+ 'is_active': self.is_active,
+ 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
+ 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None
+ }
+
diff --git a/templates/admin/site/create.html b/templates/admin/site/create.html
index f32bed6..ed0597f 100644
--- a/templates/admin/site/create.html
+++ b/templates/admin/site/create.html
@@ -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 %}
diff --git a/templates/admin/site/edit.html b/templates/admin/site/edit.html
index aef6d86..6594b5f 100644
--- a/templates/admin/site/edit.html
+++ b/templates/admin/site/edit.html
@@ -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 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生成主要功能';
- const tagsStatusDiv = document.createElement('div');
- tagsStatusDiv.className = 'tags-status';
+ const featuresStatusDiv = document.createElement('div');
+ featuresStatusDiv.className = 'tags-status';
- tagsField.parentNode.appendChild(generateBtn);
- tagsField.parentNode.appendChild(tagsStatusDiv);
+ featuresField.parentNode.appendChild(generateFeaturesBtn);
+ featuresField.parentNode.appendChild(featuresStatusDiv);
- generateBtn.addEventListener('click', function() {
+ 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) {
- showTagsStatus('请先填写网站名称和描述', 'error');
+ showFeaturesStatus('请先填写网站名称和描述', 'error');
return;
}
// 显示加载状态
- generateBtn.disabled = true;
- generateBtn.classList.add('loading');
- tagsStatusDiv.style.display = 'none';
+ generateFeaturesBtn.disabled = true;
+ generateFeaturesBtn.classList.add('loading');
+ featuresStatusDiv.style.display = 'none';
- // 调用API生成标签
- fetch('/api/generate-tags', {
+ // 调用API生成功能
+ fetch('/api/generate-features', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
- description: description
+ description: description,
+ 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.features) {
+ // 自动填充到features字段
+ featuresField.value = data.features;
+ showFeaturesStatus('✓ AI已生成主要功能列表', 'success');
} else {
- showTagsStatus('✗ ' + (data.message || '标签生成失败'), 'error');
+ showFeaturesStatus('✗ ' + (data.message || '功能生成失败'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
- showTagsStatus('✗ 网络请求失败,请重试', 'error');
+ showFeaturesStatus('✗ 网络请求失败,请重试', 'error');
})
.finally(() => {
- generateBtn.disabled = false;
- generateBtn.classList.remove('loading');
+ generateFeaturesBtn.disabled = false;
+ generateFeaturesBtn.classList.remove('loading');
});
});
- function showTagsStatus(message, type) {
- tagsStatusDiv.textContent = message;
- tagsStatusDiv.className = 'tags-status ' + type;
- tagsStatusDiv.style.display = 'block';
+ function showFeaturesStatus(message, type) {
+ featuresStatusDiv.textContent = message;
+ featuresStatusDiv.className = 'tags-status ' + type;
+ featuresStatusDiv.style.display = 'block';
}
}
});
diff --git a/templates/base_new.html b/templates/base_new.html
index 627d8e6..1252bec 100644
--- a/templates/base_new.html
+++ b/templates/base_new.html
@@ -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>
diff --git a/templates/detail_new.html b/templates/detail_new.html
index 1033f7c..c84192f 100644
--- a/templates/detail_new.html
+++ b/templates/detail_new.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>
--
2.50.1.windows.1