对于Markdown富文本实现评论和点赞系统的技术细节

8次阅读
没有评论

共计 33327 个字符,预计需要花费 84 分钟才能阅读完成。

导入

对于 Markdown,或像是 Hydro 的 主页 等可以使用 富文本 等来书写一些 介绍 文字但没有 讨论 等互动系统的,要是我们也想实现 讨论和点赞 功能怎么办?看似很难的要求,实则 解决方案十分简单

解决方案

具体思路是这样的:由于富文本支持 超链接 动态获取网络图片 ,我们只需要准备一台 服务器 ,随后在上面部署好一套 后端,实现两个功能:

  • 支持生成和返回svg 图片
  • 支持 点赞

如此一来,想要在 Markdown 里使用这个点赞的系统,只需要这样:

[![ 为我点赞 ](http:// 你的服务器 / 自定义的点赞图片 =40x)](http:// 你的服务器 / 点赞地址)

已有 ![](http:// 你的服务器 / 获取 svg 地址) 人为我点赞

Markdown

相关代码

Python 服务器 部分代码(来自Deepseek):

# ----------------------------------------- 点赞 Markdown -----------------------------------------
# 文件路径
COUNTER_FILE = Path("zans.txt")

def load_likes():
    """从文件加载点赞数"""
    if not COUNTER_FILE.exists():
        return {}
    
    likes = {}
    try:
        with open(COUNTER_FILE, 'r', encoding='utf-8') as f:
            for line in f:
                if line.strip():
                    parts = line.strip().split(':', 1)
                    if len(parts) == 2:
                        article_id = parts[0].strip()
                        try:
                            likes[article_id] = int(parts[1].strip())
                        except ValueError:
                            likes[article_id] = 0
    except Exception as e:
        print(f"Error loading likes: {e}")
        likes = {}
    
    return likes

def save_likes(likes_dict):
    """保存点赞数到文件"""
    try:
        # 先备份原文件
        if COUNTER_FILE.exists():
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            backup_file = COUNTER_FILE.parent / f"zans_backup_{timestamp}.txt"
            COUNTER_FILE.rename(backup_file)
        
        # 写入新文件
        with open(COUNTER_FILE, 'w', encoding='utf-8') as f:
            for article_id, count in likes_dict.items():
                f.write(f"{article_id}:{count}\n")
        
        # 删除旧备份(只保留最新的 5 个)
        backup_files = sorted(COUNTER_FILE.parent.glob("zans_backup_*.txt"))
        for old_backup in backup_files[:-5]:
            old_backup.unlink()
            
        return True
    except Exception as e:
        print(f"Error saving likes: {e}")
        return False

def increment_like(article_id):
    """增加指定文章的点赞数"""
    likes = load_likes()
    current_count = likes.get(article_id, 0)
    likes[article_id] = current_count + 1
    return save_likes(likes)

def get_like_count(article_id):
    """获取指定文章的点赞数"""
    likes = load_likes()
    return likes.get(article_id, 0)

@app.route('/like/<path:article_id>', methods=['GET'])
def handle_like(article_id):
    """
    处理点赞请求
    增加点赞数后重定向回文章页面
    """
    # 获取来源页面(如果提供了 referrer)
    referrer = request.referrer or f"/article/{article_id}"
    
    ref = request.args.get("redirect")
    if ref != None:
        referrer = ref
    
    # 增加点赞数
    increment_like(article_id)
    
    # 重定向回来源页面
    return redirect(referrer)

@app.route('/like/counter/<path:article_id>.svg', methods=['GET'])
def like_counter_svg(article_id):
    """
    生成点赞数 SVG 图片
    只返回 SVG 格式的图片
    """
    # 获取点赞数
    count = get_like_count(article_id)
    
    # 获取自定义参数(可选)
    color = request.args.get('color', '000000')  # 文字颜色
    bgcolor = request.args.get('bgcolor', 'ffffff')  # 背景颜色
    size = int(request.args.get('size', 14))  # 字体大小
    
    # 生成 SVG
    svg_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="{len(str(count)) * size * 0.6 + 40}" height="{size + 10}" viewBox="0 0 {len(str(count)) * size * 0.6 + 40} {size + 10}">
    <!-- 背景(可选)-->
    <rect x="0" y="0" width="100%" height="100%" fill="#{bgcolor}" rx="3" ry="3"/>
    
    <!-- 点赞图标 -->
    <text x="15" y="{size + 3}" font-family="Arial, sans-serif" font-size="{size}" fill="#{color}" style="font-weight: bold;">

    </text>
    
    <!-- 点赞数 -->
    <text x="35" y="{size + 3}" font-family="Arial, sans-serif" font-size="{size}" fill="#{color}">
        {count}
    </text>
</svg>'''
    
    # 返回 SVG 图片
    return Response(
        svg_content,
        mimetype='image/svg+xml',
        headers={
            'Cache-Control': 'no-cache, no-store, must-revalidate',
            'Pragma': 'no-cache',
            'Expires': '0'
        }
    )

@app.route('/like/counter/<path:article_id>', methods=['GET'])
def like_counter_redirect(article_id):
    """
    点赞计数器的重定向端点(兼容旧 URL)
    重定向到 SVG 版本
    """
    return redirect(f'/like/counter/{article_id}.svg?{request.query_string}')

@app.route('/like/status/<path:article_id>', methods=['GET'])
def like_status(article_id):
    """
    返回点赞状态的纯文本信息(用于调试)
    """
    count = get_like_count(article_id)
    return f" 文章 {article_id} 的点赞数: {count}"

@app.route('/like/admin/reset/<path:article_id>', methods=['GET'])
def reset_likes(article_id):
    """
    重置指定文章的点赞数(管理员功能)
    """
    likes = load_likes()
    likes[article_id] = 0
    save_likes(likes)
    return redirect(f'/like/status/{article_id}')

@app.route('/like/admin/list', methods=['GET'])
def list_all_likes():
    """
    列出所有文章的点赞数(管理员功能)
    """
    likes = load_likes()
    if not likes:
        return "暂无点赞数据"
    
    result = ["所有文章的点赞数:", "=" * 30]
    for article_id, count in sorted(likes.items()):
        result.append(f"{article_id}: {count}")
    
    return "\n".join(result)


"""
评论系统修复版
修复了重定向问题和文件保存问题
"""

# ==================== 评论系统常量 ====================
COMMENTS_FILE = Path("comments.txt")
ADMIN_PASSWORD_FILE = Path("admin_pwd.txt")

# ==================== 评论数据管理 ====================
def load_comments():
    """从文件加载评论数据"""
    comments = {}
    if not COMMENTS_FILE.exists():
        return comments
    
    try:
        with open(COMMENTS_FILE, 'r', encoding='utf-8') as f:
            current_article = None
            for line in f:
                line = line.strip()
                if not line:
                    continue
                
                if line.startswith("[") and line.endswith("]"):
                    # 文章 ID 行
                    article_id = line[1:-1]
                    comments[article_id] = []
                    current_article = article_id
                elif line.startswith("#") and current_article:
                    # 评论数据行
                    try:
                        parts = line[1:].split("|", 3)
                        if len(parts) == 4:
                            comment_id, username, content, timestamp = parts
                            comment = {
                                "id": comment_id,
                                "username": username,
                                "content": content,
                                "timestamp": timestamp,
                                "visible": True
                            }
                            comments[current_article].append(comment)
                    except ValueError:
                        continue
    except Exception as e:
        print(f"Error loading comments: {e}")
        import traceback
        traceback.print_exc()
    
    return comments

def save_comments(comments_dict):
    """保存评论数据到文件"""
    try:
        # 创建备份(如果原文件存在)
        if COMMENTS_FILE.exists():
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            backup_file = COMMENTS_FILE.parent / f"comments_backup_{timestamp}.txt"
            import shutil
            shutil.copy2(COMMENTS_FILE, backup_file)
            
            # 清理旧备份(保留最新的 3 个)
            backup_files = sorted(COMMENTS_FILE.parent.glob("comments_backup_*.txt"))
            for old_backup in backup_files[:-3]:
                try:
                    old_backup.unlink()
                except:
                    pass
        
        # 写入新文件
        with open(COMMENTS_FILE, 'w', encoding='utf-8') as f:
            for article_id, comment_list in comments_dict.items():
                if comment_list:  # 只写入有评论的文章
                    f.write(f"[{article_id}]\n")
                    for comment in comment_list:
                        if comment.get("visible", True):
                            f.write(f"#{comment['id']}|{comment['username']}|{comment['content']}|{comment['timestamp']}\n")
                    f.write("\n")
        
        return True
    except Exception as e:
        print(f"Error saving comments: {e}")
        import traceback
        traceback.print_exc()
        return False

def add_comment(article_id, username, content):
    """添加新评论"""
    comments = load_comments()
    
    # 如果文章没有评论记录,创建空列表
    if article_id not in comments:
        comments[article_id] = []
    
    # 生成评论 ID(基于时间和随机数)
    comment_id = f"c{datetime.now().strftime('%Y%m%d%H%M%S')}_{secrets.token_hex(3)}"
    
    comment = {
        "id": comment_id,
        "username": username.strip() or "匿名用户",
        "content": content.strip(),
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "visible": True
    }
    
    comments[article_id].append(comment)
    success = save_comments(comments)
    return comment_id if success else None

def get_comments_count(article_id):
    """获取评论数量"""
    comments = load_comments()
    if article_id not in comments:
        return 0
    return len([c for c in comments[article_id] if c.get("visible", True)])

def parse_markdown_text(text):
    """
    解析 Markdown 格式的文本,返回带有样式信息的片段列表
    支持:
    - 粗体:** 文本 ** 或 __文本__
    - 斜体:* 文本 * 或 _文本_
    - 高亮:== 文本 ==
    """
    import re
    
    # 定义 Markdown 模式
    patterns = [
        (r'\*\*(.*?)\*\*', 'bold'),        # 粗体:**text**
        (r'__(.*?)__', 'bold'),            # 粗体:__text__
        (r'\*(.*?)\*', 'italic'),          # 斜体:*text*
        (r'_(.*?)_', 'italic'),            # 斜体:_text_
        (r'==(.*?)==', 'highlight')        # 高亮:==text==
    ]
    
    # 解析文本
    fragments = []
    pos = 0
    text_len = len(text)
    
    while pos < text_len:
        # 查找下一个 Markdown 标记
        earliest_match = None
        earliest_pos = text_len
        
        for pattern, style_type in patterns:
            match = re.search(pattern, text[pos:])
            if match:
                match_start = pos + match.start()
                if match_start < earliest_pos:
                    earliest_pos = match_start
                    earliest_match = (pattern, style_type, match)
        
        if earliest_match and earliest_pos < text_len:
            # 添加普通文本(如果有)
            if earliest_pos > pos:
                plain_text = text[pos:earliest_pos]
                fragments.append({
                    'text': plain_text,
                    'style': 'normal'
                })
            
            # 添加样式文本
            pattern, style_type, match = earliest_match
            styled_text = match.group(1)
            fragments.append({
                'text': styled_text,
                'style': style_type
            })
            
            # 更新位置
            pos = earliest_pos + len(match.group(0))
        else:
            # 添加剩余的普通文本
            if pos < text_len:
                plain_text = text[pos:]
                fragments.append({
                    'text': plain_text,
                    'style': 'normal'
                })
            break
    
    # 如果没有样式片段,返回单个普通片段
    if not fragments:
        fragments.append({
            'text': text,
            'style': 'normal'
        })
    
    return fragments

def calculate_text_width(text, font_size=12):
    """
    估算文本宽度(近似值)
    英文字符:0.6 * 字体大小
    中文字符:1.0 * 字体大小
    """
    width = 0
    for char in text:
        if '\u4e00' <= char <= '\u9fff':  # 中文字符
            width += font_size
        else:  # 英文字符
            width += font_size * 0.6
    return width

def generate_comments_svg(comments, limit=None):
    """生成带用户头像和 Markdown 格式支持的评论列表 SVG"""
    if not comments:
        return generate_no_comments_svg()
    
    # 限制显示条数
    if limit and len(comments) > limit:
        display_comments = comments[:limit]
        show_more = True
    else:
        display_comments = comments
        show_more = False
    
    # 预定义一些好看的背景颜色
    avatar_colors = [
        '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57',
        '#FF9FF3', '#54A0FF', '#5F27CD', '#00D2D3', '#FF9F43',
        '#1DD1A1', '#FF9F43', '#2ECC71', '#9B59B6', '#3498DB'
    ]
    
    # 定义 Markdown 样式
    markdown_styles = {
        'bold': {
            'font_weight': 'bold',
            'font_size': 12,
            'fill': '#212529'
        },
        'italic': {
            'font_style': 'italic',
            'font_size': 12,
            'fill': '#212529'
        },
        'highlight': {
            'font_size': 12,
            'fill': '#212529'
        },
        'normal': {
            'font_size': 12,
            'fill': '#212529'
        }
    }
    
    # 计算 SVG 高度
    header_height = 40
    footer_height = 30 if show_more else 10
    
    # 动态计算每行评论的高度
    line_heights = []
    line_contents = []  # 存储每行评论的解析内容
    
    for comment in display_comments:
        content = comment['content']
        
        # 解析 Markdown
        fragments = parse_markdown_text(content)
        
        # 计算内容行数
        line_count = 1
        current_line_width = 0
        max_line_width = 320  # 总宽度 400 - 左边距 50 - 右边距 30
        
        for fragment in fragments:
            fragment_width = calculate_text_width(fragment['text'], 12)
            if current_line_width + fragment_width > max_line_width:
                line_count += 1
                current_line_width = fragment_width
            else:
                current_line_width += fragment_width
        
        # 基本行高 = 头像高度 + 用户名和内容间距
        line_height = 50 + (line_count * 20)
        line_heights.append(line_height)
        line_contents.append(fragments)
    
    total_height = header_height + sum(line_heights) + footer_height
    
    svg_lines = [f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="{total_height}" viewBox="0 0 400 {total_height}">
    <!-- 背景 -->
    <rect width="100%" height="100%" fill="#f8f9fa" rx="5" ry="5"/>
    
    <!-- 标题栏 -->
    <rect x="0" y="0" width="100%" height="30" fill="#007bff" rx="5" ry="5"/>
    <text x="10" y="20" font-family="Arial, sans-serif" font-size="14" fill="white" font-weight="bold">
        ✎ 评论 ({len(comments)}条)
    </text>''']
    
    # 当前 Y 坐标
    current_y = 40
    
    # 添加评论
    for i, comment in enumerate(display_comments):
        # 选择头像颜色(基于用户名哈希)
        import hashlib
        if comment['username']:
            color_index = int(hashlib.md5(comment['username'].encode()).hexdigest()[:8], 16) % len(avatar_colors)
        else:
            color_index = 0
        avatar_color = avatar_colors[color_index]
        
        # 获取用户首字母(用于头像)
        if comment['username']:
            first_char = comment['username'][0].upper()
        else:
            first_char = "?"
        
        # 头像圆
        svg_lines.append(f'''
    <!-- 评论 {i+1} 的头像 -->
    <circle cx="25" cy="{current_y + 15}" r="18" fill="{avatar_color}"/>
    <text x="25" y="{current_y + 21}" font-family="Arial, sans-serif" font-size="14" fill="white" 
          text-anchor="middle" font-weight="bold">
        {first_char}
    </text>''')
        
        # 用户名和时间
        svg_lines.append(f'''
    <!-- 评论 {i+1} 的用户信息 -->
    <text x="50" y="{current_y + 10}" font-family="Arial, sans-serif" font-size="13" fill="#495057" font-weight="bold">
        {comment['username']}
    </text>
    <text x="50" y="{current_y + 25}" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">
        {comment['timestamp']}
    </text>''')
        
        # 评论内容(支持 Markdown)
        content_start_x = 50
        content_start_y = current_y + 40
        
        fragments = line_contents[i]
        
        # 分页绘制文本
        line_index = 0
        current_x = content_start_x
        current_y_line = content_start_y
        max_line_width = 320
        
        # 绘制高亮背景(先绘制所有高亮背景)
        highlight_rectangles = []
        temp_x = content_start_x
        temp_y_line = content_start_y
        
        for fragment in fragments:
            if fragment['style'] == 'highlight':
                fragment_width = calculate_text_width(fragment['text'], 12)
                # 检查是否需要换行
                if temp_x + fragment_width > max_line_width:
                    temp_y_line += 20
                    temp_x = content_start_x
                
                highlight_rectangles.append({
                    'x': temp_x,
                    'y': temp_y_line - 10,
                    'width': fragment_width + 4,
                    'height': 16
                })
                temp_x += fragment_width
            else:
                fragment_width = calculate_text_width(fragment['text'], 12)
                if temp_x + fragment_width > max_line_width:
                    temp_y_line += 20
                    temp_x = content_start_x
                temp_x += fragment_width
        
        # 绘制高亮矩形
        for rect in highlight_rectangles:
            svg_lines.append(f'''
    <rect x="{rect['x'] - 2}" y="{rect['y']}" width="{rect['width']}" height="{rect['height']}" 
          fill="#FFEB3B" opacity="0.5" rx="2" ry="2"/>''')
        
        # 绘制文本片段
        for fragment in fragments:
            style_type = fragment['style']
            text = fragment['text']
            fragment_width = calculate_text_width(text, 12)
            
            # 检查是否需要换行
            if current_x + fragment_width > max_line_width:
                line_index += 1
                current_y_line += 20
                current_x = content_start_x
            
            # 构建样式属性
            style_attrs = []
            style_config = markdown_styles[style_type]
            
            if style_type == 'bold':
                style_attrs.append('font-weight="bold"')
            elif style_type == 'italic':
                style_attrs.append('font-style="italic"')
            
            # 设置字体大小和颜色
            style_attrs.append(f'font-size="{style_config["font_size"]}"')
            style_attrs.append(f'fill="{style_config["fill"]}"')
            
            # 合并样式属性
            style_str = " ".join(style_attrs)
            
            # 绘制文本片段
            svg_lines.append(f'''
    <text x="{current_x}" y="{current_y_line}" font-family="Arial, sans-serif" {style_str}>
        {text}
    </text>''')
            
            current_x += fragment_width
        
        # 分隔线
        if i < len(display_comments) - 1:
            svg_lines.append(f'''
    <line x1="10" y1="{current_y + line_heights[i] - 5}" x2="390" y2="{current_y + line_heights[i] - 5}" 
          stroke="#dee2e6" stroke-width="1"/>''')
        
        current_y += line_heights[i]
    
    # 显示更多提示
    if show_more:
        svg_lines.append(f'''
    <text x="50%" y="{total_height - 10}" text-anchor="middle" font-family="Arial, sans-serif" 
          font-size="12" fill="#007bff">
        ↓ 查看更多评论(还有 {len(comments) - limit} 条)→
    </text>''')
    
    # 添加点击区域
    if show_more:
        svg_lines.append(f'''
    <rect x="0" y="{total_height - 25}" width="400" height="25" fill="transparent" opacity="0">
        <title> 点击查看所有评论 </title>
    </rect>''')
    
    svg_lines.append("</svg>")
    return "\n".join(svg_lines)

def generate_no_comments_svg():
    """生成无评论的 SVG"""
    svg = '''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="150">
    <rect width="100%" height="100%" fill="#f8f9fa" rx="5" ry="5"/>
    
    <!-- 头像 -->
    <circle cx="200" cy="50" r="25" fill="#dee2e6"/>
    <text x="200" y="55" text-anchor="middle" font-family="Arial, sans-serif" 
          font-size="20" fill="#adb5bd" font-weight="bold">
        ?
    </text>
    
    <!-- 提示信息 -->
    <text x="200" y="100" text-anchor="middle" font-family="Arial, sans-serif" 
          font-size="16" fill="#6c757d" font-weight="bold">
        暂无评论
    </text>
    
    <text x="200" y="120" text-anchor="middle" font-family="Arial, sans-serif" 
          font-size="12" fill="#adb5bd">
        点击发表第一条评论
    </text>
    
    <!-- 示例 Markdown 提示 -->
    <text x="200" y="135" text-anchor="middle" font-family="Arial, sans-serif" 
          font-size="10" fill="#6c757d" opacity="0.7">
        支持: ** 粗体 ** * 斜体 * == 高亮 ==
    </text>
    
    <!-- 点击区域 -->
    <rect x="0" y="0" width="400" height="150" fill="transparent" opacity="0">
        <title> 点击发表评论 </title>
    </rect>
</svg>'''
    return svg

# ==================== Flask 路由 ====================
@app.route('/comment/<path:article_id>')
def comment_form(article_id):
    """显示评论表单页面"""
    redirect_url = request.args.get('redirect', '/')
    
    return f'''
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 发表评论 - {article_id}</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; 
                max-width: 600px; margin: 0 auto; padding: 20px; background: #f5f5f5; }}
        .form-container {{ background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
        h1 {{ color: #333; margin-bottom: 20px; }}
        .form-group {{ margin-bottom: 20px; }}
        label {{ display: block; margin-bottom: 8px; font-weight: 600; color: #555; }}
        input, textarea {{ 
            width: 100%; 
            padding: 12px; 
            border: 2px solid #ddd; 
            border-radius: 6px; 
            font-size: 16px;
            transition: border-color 0.3s;
        }}
        input:focus, textarea:focus {{ 
            outline: none; 
            border-color: #007bff; 
            box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
        }}
        button {{ 
            background: linear-gradient(135deg, #007bff, #0056b3);
            color: white; 
            padding: 14px 28px; 
            border: none; 
            border-radius: 6px; 
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
            width: 100%;
        }}
        button:hover {{ 
            transform: translateY(-2px); 
            box-shadow: 0 4px 15px rgba(0,123,255,0.3);
        }}
        .back-link {{ 
            display: inline-block; 
            margin-top: 20px; 
            color: #666; 
            text-decoration: none;
            padding: 10px;
            border-radius: 5px;
            transition: background 0.3s;
        }}
        .back-link:hover {{ background: #f0f0f0; }}
        .info-box {{ 
            background: #e7f3ff; 
            border-left: 4px solid #007bff; 
            padding: 15px; 
            margin-bottom: 20px;
            border-radius: 0 5px 5px 0;
        }}
        .char-count {{ 
            text-align: right; 
            font-size: 12px; 
            color: #666; 
            margin-top: 5px;
        }}
    </style>
    <script>
        function updateCharCount() {{
            var textarea = document.getElementById('content');
            var count = document.getElementById('charCount');
            count.textContent = textarea.value.length + '/500';
            
            if (textarea.value.length > 500) {{
                count.style.color = '#dc3545';
            }} else {{
                count.style.color = '#666';
            }}
        }}
    </script>
</head>
<body>
    <div class="form-container">
        <h1>✎ 发表评论 </h1>
        
        <div class="info-box">
            <p><strong> 文章:</strong>{article_id}</p>
            <p><strong> 注意:</strong> 请文明发言,尊重他人观点 </p>
        </div>
        
        <form action="/comment/{article_id}/submit?redirect={redirect_url}" method="post">
            <input type="hidden" name="article_id" value="{article_id}">
            
            <div class="form-group">
                <label for="username">👤 昵称(可选):</label>
                <input type="text" id="username" name="username" placeholder=" 匿名用户 ">
            </div>
            
            <div class="form-group">
                <label for="content">💭 评论内容:</label>
                <textarea id="content" name="content" rows="6" 
                          placeholder=" 请输入您的评论..." 
                          maxlength="500" 
                          oninput="updateCharCount()" 
                          required></textarea>
                <div class="char-count" id="charCount">0/500</div>
            </div>
            
            <button type="submit">📤 提交评论 </button>
        </form>
        
        <a href="{redirect_url}" class="back-link">← 返回文章 </a>
    </div>
</body>
</html>'''

@app.route('/comment/<path:article_id>/submit', methods=['POST'])
def submit_comment(article_id):
    """提交评论"""
    username = request.form.get('username', '').strip()
    content = request.form.get('content', '').strip()
    redirect_url = request.args.get('redirect', '/')
    
    if not content:
        # 如果内容为空,返回错误页面
        return '''
        <!DOCTYPE html>
        <html>
        <head><title> 评论提交失败 </title></head>
        <body>
            <h1> 评论提交失败 </h1>
            <p> 评论内容不能为空。</p>
            <a href="javascript:history.back()"> 返回修改 </a>
        </body>
        </html>''', 400
    
    # 限制内容长度
    if len(content) > 500:
        content = content[:500]
    
    # 添加评论
    comment_id = add_comment(article_id, username, content)
    
    if comment_id:
        # 重定向回指定页面
        return redirect(redirect_url)
    else:
        return '''
        <!DOCTYPE html>
        <html>
        <head><title> 评论提交失败 </title></head>
        <body>
            <h1> 评论提交失败 </h1>
            <p> 保存评论时出现错误,请稍后重试。</p>
            <a href="javascript:history.back()"> 返回修改 </a>
        </body>
        </html>''', 500

@app.route('/comment/<path:article_id>/preview.svg')
def comment_preview_svg(article_id):
    """生成评论预览 SVG(显示最新 5 条评论)"""
    comments = load_comments()
    article_comments = comments.get(article_id, [])
    
    # 过滤可见评论并按时间倒序排列
    visible_comments = [c for c in article_comments if c.get("visible", True)]
    visible_comments.sort(key=lambda x: x['timestamp'], reverse=True)
    
    svg_content = generate_comments_svg(visible_comments, limit=5)
    
    return Response(
        svg_content,
        mimetype='image/svg+xml',
        headers={
            'Cache-Control': 'max-age=60',  # 缓存 60 秒
            'Pragma': 'public',
        }
    )

@app.route('/comment/<path:article_id>/list.svg')
def comment_list_svg(article_id):
    """生成完整评论列表 SVG"""
    comments = load_comments()
    article_comments = comments.get(article_id, [])
    
    # 过滤可见评论并按时间倒序排列
    visible_comments = [c for c in article_comments if c.get("visible", True)]
    visible_comments.sort(key=lambda x: x['timestamp'], reverse=True)
    
    svg_content = generate_comments_svg(visible_comments)
    
    return Response(
        svg_content,
        mimetype='image/svg+xml',
        headers={
            'Cache-Control': 'max-age=60',
            'Pragma': 'public',
        }
    )

@app.route('/comment/<path:article_id>/list')
def comment_list_html(article_id):
    """显示 HTML 格式的评论列表"""
    comments = load_comments()
    article_comments = comments.get(article_id, [])
    
    # 过滤可见评论并按时间倒序排列
    visible_comments = [c for c in article_comments if c.get("visible", True)]
    visible_comments.sort(key=lambda x: x['timestamp'], reverse=True)
    
    html = f'''<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 评论列表 - {article_id}</title>
    <style>
        body {{ 
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; 
            max-width: 800px; 
            margin: 0 auto; 
            padding: 20px; 
            background: #f5f5f5;
            line-height: 1.6;
        }}
        .container {{ 
            background: white; 
            padding: 30px; 
            border-radius: 10px; 
            box-shadow: 0 2px 15px rgba(0,0,0,0.1);
        }}
        h1 {{ 
            color: #333; 
            border-bottom: 3px solid #007bff; 
            padding-bottom: 10px; 
            margin-bottom: 20px;
        }}
        .stats {{ 
            background: #e7f3ff; 
            padding: 15px; 
            border-radius: 5px; 
            margin-bottom: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }}
        .comment {{ 
            border-bottom: 1px solid #eee; 
            padding: 20px 0; 
            animation: fadeIn 0.3s ease-in;
        }}
        @keyframes fadeIn {{
            from {{ opacity: 0; transform: translateY(10px); }}
            to {{ opacity: 1; transform: translateY(0); }}
        }}
        .comment-header {{ 
            display: flex; 
            justify-content: space-between; 
            align-items: center;
            margin-bottom: 10px;
        }}
        .user-info {{ display: flex; align-items: center; }}
        .avatar {{ 
            width: 40px; 
            height: 40px; 
            background: linear-gradient(135deg, #007bff, #00d4ff);
            border-radius: 50%; 
            display: flex; 
            align-items: center; 
            justify-content: center;
            color: white;
            font-weight: bold;
            margin-right: 10px;
        }}
        .username {{ 
            font-weight: bold; 
            color: #333; 
            font-size: 16px;
        }}
        .timestamp {{ 
            color: #666; 
            font-size: 14px;
        }}
        .content {{ 
            margin-top: 10px; 
            color: #444;
            font-size: 16px;
            white-space: pre-wrap;
            word-wrap: break-word;
        }}
        .empty-state {{ 
            text-align: center; 
            padding: 60px 20px; 
            color: #666;
        }}
        .actions {{ 
            margin-top: 20px; 
            padding-top: 20px;
            border-top: 1px solid #eee;
        }}
        .btn {{ 
            display: inline-block; 
            padding: 12px 24px; 
            margin-right: 10px; 
            background: #007bff; 
            color: white; 
            text-decoration: none; 
            border-radius: 6px;
            font-weight: 600;
            transition: all 0.3s;
        }}
        .btn:hover {{ 
            background: #0056b3; 
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0,123,255,0.3);
        }}
        .btn-secondary {{ background: #6c757d; }}
        .btn-secondary:hover {{ background: #545b62; }}
        .pagination {{ 
            text-align: center; 
            margin-top: 30px;
        }}
        .page-btn {{ 
            display: inline-block; 
            padding: 8px 16px; 
            margin: 0 5px; 
            border: 1px solid #ddd; 
            text-decoration: none; 
            color: #007bff;
            border-radius: 4px;
        }}
        .page-btn.active {{ 
            background: #007bff; 
            color: white; 
            border-color: #007bff;
        }}
    </style>
</head>
<body>
    <div class="container">
        <h1>✎ 评论列表 </h1>
        
        <div class="stats">
            <div>
                <strong> 文章:</strong>{article_id}
            </div>
            <div>
                <strong> 评论总数:</strong>{len(visible_comments)}
            </div>
        </div>
        
        <div class="actions">
            <a href="/comment/{article_id}?redirect={request.args.get("redirect") or request.referrer or '/'}" class="btn">✏️ 发表评论 </a>
            <a href="{request.args.get("redirect") or request.referrer or '/'}" class="btn btn-secondary">← 返回文章 </a>
        </div>
        
        <hr style="margin: 30px 0;">'''
    
    if not visible_comments:
        html += '''
        <div class="empty-state">
            <h3 style="color: #999; margin-bottom: 10px;">✎ 暂无评论 </h3>
            <p style="color: #777;"> 成为第一个评论的人吧!</p>
        </div>'''
    else:
        for comment in visible_comments:
            # 获取用户首字母作为头像
            first_char = comment['username'][0].upper() if comment['username'] else '?'
            
            html += f'''
        <div class="comment">
            <div class="comment-header">
                <div class="user-info">
                    <div class="avatar">{first_char}</div>
                    <div>
                        <div class="username">{comment['username']}</div>
                        <div class="timestamp">⏰ {comment['timestamp']}</div>
                    </div>
                </div>
                <div style="font-size: 12px; color: #888;">
                    ID: {comment['id']}
                </div>
            </div>
            <div class="content">
                {comment['content']}
            </div>
        </div>'''
    
    html += '''
        <div class="actions">
            <p style="text-align: center; color: #666;">
                SVG 评论预览:<a href="/comment/{}/preview.svg" target="_blank"> 点击查看 </a>
            </p>
        </div>
    </div>
</body>
</html>'''.format(article_id)
    
    return html

# ==================== 管理员面板 ====================
def init_admin_password():
    """初始化管理员密码"""
    if not ADMIN_PASSWORD_FILE.exists():
        default_password = secrets.token_hex(8)  # 生成随机密码
        with open(ADMIN_PASSWORD_FILE, 'w', encoding='utf-8') as f:
            f.write(default_password)
        print(f" 初始管理员密码: {default_password}")
        print("请保存此密码,然后访问 /comment/admin/login 登录")

def check_admin_session():
    """检查管理员会话"""
    token = request.args.get('token') or request.form.get('token')
    if not token:
        return False
    
    # 简单的会话检查(生产环境应使用更安全的方式)
    session_file = Path(f"admin_session_{token}.txt")
    if session_file.exists():
        # 检查会话是否过期(24 小时)
        import time
        if time.time() - session_file.stat().st_mtime < 86400:
            return True
    
    return False

@app.route('/comment/admin/login')
def admin_login_page():
    """管理员登录页面"""
    return '''
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 评论管理登录 </title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .login-box {
            background: white; 
            padding: 40px; 
            border-radius: 10px; 
            box-shadow: 0 15px 35px rgba(0,0,0,0.2);
            width: 100%;
            max-width: 400px;
        }
        h2 {
            color: #333; 
            margin-bottom: 30px; 
            text-align: center;
        }
        .input-group {
            margin-bottom: 20px; 
        }
        input {
            width: 100%; 
            padding: 15px; 
            border: 2px solid #ddd; 
            border-radius: 6px; 
            font-size: 16px;
            transition: border-color 0.3s;
        }
        input:focus {
            outline: none; 
            border-color: #667eea; 
        }
        button {
            width: 100%; 
            padding: 15px; 
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white; 
            border: none; 
            border-radius: 6px; 
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s;
        }
        button:hover {
            transform: translateY(-2px); 
        }
        .error {
            color: #dc3545; 
            margin-top: 10px; 
            text-align: center;
        }
        .logo {
            text-align: center; 
            font-size: 48px; 
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="login-box">
        <div class="logo">🔐</div>
        <h2> 评论管理登录 </h2>
        <form action="/comment/admin/verify" method="post">
            <div class="input-group">
                <input type="password" name="password" placeholder=" 管理员密码 " required>
            </div>
            <button type="submit"> 登录管理面板 </button>
        </form>
    </div>
</body>
</html>'''

@app.route('/comment/admin/verify', methods=['POST'])
def admin_verify():
    """验证管理员密码"""
    password = request.form.get('password', '')
    
    # 读取密码文件
    if ADMIN_PASSWORD_FILE.exists():
        with open(ADMIN_PASSWORD_FILE, 'r', encoding='utf-8') as f:
            correct_password = f.read().strip()
        
        if password == correct_password:
            # 生成管理会话令牌
            session_token = secrets.token_hex(16)
            session_file = Path(f"admin_session_{session_token}.txt")
            session_file.touch()
            
            return redirect(f'/comment/admin?token={session_token}')
    
    return '''
    <!DOCTYPE html>
    <html>
    <head><title> 登录失败 </title></head>
    <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
        <h1 style="color: #dc3545;">❌ 密码错误 </h1>
        <p> 请检查密码后重试。</p>
        <a href="/comment/admin/login" style="color: #007bff;"> 返回登录 </a>
    </body>
    </html>''', 401

@app.route('/comment/admin')
def admin_panel():
    """评论管理面板"""
    if not check_admin_session():
        return redirect('/comment/admin/login')
    
    token = request.args.get('token', '')
    comments = load_comments()
    total_comments = sum(len(comments[aid]) for aid in comments)
    visible_comments = sum(
        len([c for c in comments[aid] if c.get("visible", True)])
        for aid in comments
    )
    
    html = f'''<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> 评论管理面板 </title>
    <style>
        body {{ 
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            margin: 0; 
            background: #f5f5f5;
        }}
        .header {{ 
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white; 
            padding: 20px; 
            margin-bottom: 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }}
        .container {{ 
            max-width: 1200px; 
            margin: 0 auto; 
            padding: 20px;
        }}
        .stats-grid {{ 
            display: grid; 
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 
            gap: 20px; 
            margin-bottom: 30px;
        }}
        .stat-card {{ 
            background: white; 
            padding: 20px; 
            border-radius: 10px; 
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }}
        .stat-card h3 {{ margin-top: 0; color: #666; }}
        .stat-number {{ 
            font-size: 36px; 
            font-weight: bold; 
            color: #667eea;
            margin: 10px 0;
        }}
        .article-list {{ 
            background: white; 
            padding: 20px; 
            border-radius: 10px; 
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            margin-top: 20px;
        }}
        .article-item {{ 
            border-bottom: 1px solid #eee; 
            padding: 15px 0;
            transition: background 0.3s;
        }}
        .article-item:hover {{ background: #f8f9fa; }}
        .comment-item {{ 
            padding: 10px; 
            background: #f8f9fa; 
            margin: 5px 0; 
            border-radius: 5px;
            border-left: 3px solid #007bff;
        }}
        .comment-header {{ 
            display: flex; 
            justify-content: space-between; 
            align-items: center;
            margin-bottom: 5px;
        }}
        .actions {{ 
            margin-top: 10px; 
            display: flex; 
            gap: 5px;
            flex-wrap: wrap;
        }}
        .btn {{ 
            padding: 5px 10px; 
            border: none; 
            border-radius: 3px; 
            cursor: pointer; 
            font-size: 12px;
            transition: all 0.3s;
        }}
        .btn-delete {{ background: #dc3545; color: white; }}
        .btn-delete:hover {{ background: #c82333; }}
        .btn-edit {{ background: #ffc107; color: black; }}
        .btn-edit:hover {{ background: #e0a800; }}
        .btn-toggle {{ background: #6c757d; color: white; }}
        .btn-toggle:hover {{ background: #5a6268; }}
        .btn-refresh {{ 
            background: #28a745; 
            color: white; 
            padding: 10px 20px;
            font-size: 14px;
        }}
        .btn-refresh:hover {{ background: #218838; }}
        .search-box {{ 
            margin-bottom: 20px; 
            display: flex;
            gap: 10px;
        }}
        input[type="text"] {{ 
            flex: 1; 
            padding: 10px; 
            border: 2px solid #ddd; 
            border-radius: 5px;
            font-size: 14px;
        }}
        .status-badge {{ 
            display: inline-block; 
            padding: 2px 8px; 
            border-radius: 10px; 
            font-size: 12px; 
            margin-left: 10px;
        }}
        .visible {{ background: #d4edda; color: #155724; }}
        .hidden {{ background: #f8d7da; color: #721c24; }}
        .empty-state {{ 
            text-align: center; 
            padding: 40px; 
            color: #666;
        }}
        .controls {{ 
            display: flex; 
            justify-content: space-between; 
            align-items: center;
            margin-bottom: 20px;
            flex-wrap: wrap;
            gap: 10px;
        }}
        .action-success {{ 
            background: #d4edda; 
            color: #155724; 
            padding: 10px; 
            border-radius: 5px;
            margin-bottom: 10px;
            display: none;
        }}
    </style>
    <script>
        function showSuccess(message) {{
            var div = document.getElementById('actionSuccess');
            div.textContent = message;
            div.style.display = 'block';
            setTimeout(() => div.style.display = 'none', 3000);
        }}
        
        function searchComments() {{
            var input = document.getElementById('searchInput').value.toLowerCase();
            var articles = document.querySelectorAll('.article-item');
            
            articles.forEach(function(article) {{
                var articleText = article.textContent.toLowerCase();
                if (articleText.includes(input)) {{
                    article.style.display = 'block';
                }} else {{
                    article.style.display = 'none';
                }}
            }});
        }}
        
        function toggleComment(articleId, commentId) {{
            if (confirm(' 确定要切换评论的显示状态吗?')) {{
                var formData = new FormData();
                formData.append('token', '{token}');
                formData.append('article_id', articleId);
                formData.append('comment_id', commentId);
                
                fetch('/comment/admin/toggle', {{
                    method: 'POST',
                    body: formData
                }}).then(function(response) {{
                    if (response.ok) {{
                        showSuccess(' 评论状态已更新 ');
                        setTimeout(() => location.reload(), 1000);
                    }} else {{
                        alert(' 操作失败 ');
                    }}
                }}).catch(function(error) {{
                    alert(' 网络错误: ' + error);
                }});
            }}
        }}
        
        function deleteComment(articleId, commentId) {{
            if (confirm(' 确定要删除这条评论吗?此操作不可撤销!')) {{
                var formData = new FormData();
                formData.append('token', '{token}');
                formData.append('article_id', articleId);
                formData.append('comment_id', commentId);
                
                fetch('/comment/admin/delete', {{
                    method: 'POST',
                    body: formData
                }}).then(function(response) {{
                    if (response.ok) {{
                        showSuccess(' 评论已删除 ');
                        setTimeout(() => location.reload(), 1000);
                    }} else {{
                        alert(' 删除失败 ');
                    }}
                }}).catch(function(error) {{
                    alert(' 网络错误: ' + error);
                }});
            }}
        }}
        
        function reloadPanel() {{
            location.reload();
        }}
    </script>
</head>
<body>
    <div class="header">
        <div class="container">
            <h1>📋 评论管理面板 </h1>
            <p> 管理所有文章的评论内容 </p>
        </div>
    </div>
    
    <div class="container">
        <div id="actionSuccess" class="action-success"></div>
        
        <div class="controls">
            <div class="search-box">
                <input type="text" id="searchInput" placeholder=" 搜索文章或评论内容..." onkeyup="searchComments()">
                <button class="btn btn-refresh" onclick="reloadPanel()">🔄 刷新 </button>
            </div>
        </div>
        
        <div class="stats-grid">
            <div class="stat-card">
                <h3> 文章总数 </h3>
                <div class="stat-number">{len(comments)}</div>
                <p> 有评论的文章数量 </p>
            </div>
            <div class="stat-card">
                <h3> 评论总数 </h3>
                <div class="stat-number">{total_comments}</div>
                <p> 所有评论的数量 </p>
            </div>
            <div class="stat-card">
                <h3> 可见评论 </h3>
                <div class="stat-number">{visible_comments}</div>
                <p> 当前显示的评论 </p>
            </div>
            <div class="stat-card">
                <h3> 隐藏评论 </h3>
                <div class="stat-number">{total_comments - visible_comments}</div>
                <p> 已隐藏的评论 </p>
            </div>
        </div>
        
        <div class="article-list">'''
    
    if not comments:
        html += '''
            <div class="empty-state">
                <h3> 暂无评论数据 </h3>
                <p> 还没有任何文章的评论 </p>
            </div>'''
    else:
        for article_id, comment_list in sorted(comments.items()):
            visible_comments = [c for c in comment_list if c.get("visible", True)]
            hidden_comments = [c for c in comment_list if not c.get("visible", True)]
            
            html += f'''
            <div class="article-item">
                <h3>
{article_id} 
                    <span style="font-size: 0.9em; color: #666;">
                        (可见: {len(visible_comments)}条, 隐藏: {len(hidden_comments)}条)
                    </span>
                </h3>
                <div>
                    <a href="/comment/{article_id}/list" target="_blank">📝 查看评论页 </a> | 
                    <a href="/comment/{article_id}" target="_blank">➕ 发表评论 </a>
                </div>
                
                <div class="comments">'''
            
            for comment in comment_list:
                status = "✅ 可见" if comment.get("visible", True) else "🚫 隐藏"
                status_class = "visible" if comment.get("visible", True) else "hidden"
                
                html += f'''
                    <div class="comment-item">
                        <div class="comment-header">
                            <div>
                                <strong>{comment['username']}</strong> 
                                <span class="status-badge {status_class}">{status}</span>
                            </div>
                            <div style="color: #666; font-size: 12px;">
                                {comment['timestamp']}
                            </div>
                        </div>
                        <div>{comment['content']}</div>
                        <div class="actions">
                            <button class="btn btn-toggle" onclick="toggleComment('{article_id}', '{comment['id']}')">
                                { "隐藏 " if comment.get("visible", True) else " 显示" }
                            </button>
                            <button class="btn btn-delete" onclick="deleteComment('{article_id}', '{comment['id']}')">
                                删除
                            </button>
                            <span style="color: #888; font-size: 11px;">ID: {comment['id']}</span>
                        </div>
                    </div>'''
            
            html += '''
                </div>
            </div>'''
    
    html += '''
        </div>
        
        <div style="text-align: center; margin-top: 30px; color: #666; font-size: 14px;">
            <p> 管理令牌: <code>{}</code></p>
            <p> 最后更新: {}</p>
        </div>
    </div>
</body>
</html>'''.format(token, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    
    return html

@app.route('/comment/admin/toggle', methods=['POST'])
def admin_toggle_comment():
    """切换评论显示状态"""
    token = request.form.get('token', '')
    
    if not check_admin_session():
        return "未授权", 401
    
    article_id = request.form.get('article_id', '')
    comment_id = request.form.get('comment_id', '')
    
    comments = load_comments()
    
    if article_id in comments:
        for comment in comments[article_id]:
            if comment['id'] == comment_id:
                comment['visible'] = not comment.get('visible', True)
                success = save_comments(comments)
                if success:
                    return "OK"
                else:
                    return "保存失败", 500
    
    return "评论不存在", 404

@app.route('/comment/admin/delete', methods=['POST'])
def admin_delete_comment():
    """删除评论"""
    token = request.form.get('token', '')
    
    if not check_admin_session():
        return "未授权", 401
    
    article_id = request.form.get('article_id', '')
    comment_id = request.form.get('comment_id', '')
    
    comments = load_comments()
    
    if article_id in comments:
        original_count = len(comments[article_id])
        comments[article_id] = [c for c in comments[article_id] if c['id'] != comment_id]
        
        if len(comments[article_id]) < original_count:
            success = save_comments(comments)
            if success:
                return "OK"
            else:
                return "保存失败", 500
    
    return "评论不存在", 404

@app.route('/comment/admin/logout')
def admin_logout():
    """管理员登出"""
    token = request.args.get('token', '')
    if token:
        session_file = Path(f"admin_session_{token}.txt")
        if session_file.exists():
            session_file.unlink()
    
    return redirect('/comment/admin/login')

# ==================== 初始化函数 ====================
def init_system():
    """初始化系统"""
    # 初始化评论数据文件
    if not COMMENTS_FILE.exists():
        save_comments({
            "demo/article/123": [
                {
                    "id": "c20231201080000_abc123",
                    "username": "热心读者",
                    "content": "这篇文章非常有帮助,谢谢作者!",
                    "timestamp": "2023-12-01 08:00:00",
                    "visible": True
                },
                {
                    "id": "c20231201090000_def456",
                    "username": "技术小白",
                    "content": "讲解得很清楚,终于搞懂了这个概念。",
                    "timestamp": "2023-12-01 09:00:00",
                    "visible": True
                }
            ]
        })
        print("✓ 创建了示例评论数据")
    
    # 初始化管理员密码
    init_admin_password()
    
    print("=" * 60)
    print("评论系统初始化完成!")
    print("=" * 60)
    print("\n📋 可用功能:")
    print("-" * 40)
    print("1. 评论表单:     /comment/< 文章 ID>?redirect=< 返回 URL>")
    print("2. 评论预览 SVG:  /comment/< 文章 ID>/preview.svg")
    print("3. 完整评论 SVG:  /comment/< 文章 ID>/list.svg")
    print("4. 评论列表 HTML: /comment/< 文章 ID>/list")
    print("5. 管理面板:     /comment/admin/login")
    print("\n📝 示例:")
    print("-" * 40)
    print("文章评论页面: http://localhost:5000/comment/demo/article/123?redirect=/")
    print("评论预览 SVG:  http://localhost:5000/comment/demo/article/123/preview.svg")
    print("管理面板:     http://localhost:5000/comment/admin/login")
    print("=" * 60)

Python

正文完
 0
梨灵
版权声明:本站原创文章,由 梨灵 于2025-12-13发表,共计33327字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)