Files
zjpb.net/v2.1.0.patch

2439 lines
89 KiB
Diff
Raw Normal View History

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