921 lines
35 KiB
Python
921 lines
35 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
闲言APP — 协议HTML生成与上传脚本
|
||
创建时间: 2026-05-19
|
||
更新时间: 2026-05-19
|
||
作用: 从agreement_data.dart提取中英文协议内容,生成双语HTML文件,上传到服务器
|
||
上次更新: 支持中英文双语HTML生成,URL参数?lang=en切换语言
|
||
"""
|
||
|
||
import re
|
||
import os
|
||
import paramiko
|
||
|
||
HOST = '123.207.67.197'
|
||
PORT = 22
|
||
USER = 'root'
|
||
PASS = '520Kiss123'
|
||
|
||
BASE_DIR = r'e:\project\flutter\f\xianyan\docs\toolsapi\agreements'
|
||
REMOTE_BASE = '/www/wwwroot/tools.wktyl.com/public/agreements/'
|
||
|
||
DART_FILE = r'e:\project\flutter\f\xianyan\lib\features\agreements\data\agreement_data.dart'
|
||
|
||
AGREEMENT_MAP = {
|
||
'privacyPolicy': {
|
||
'filename': 'privacy-policy.html',
|
||
'icon': '🔒',
|
||
'title': '隐私政策',
|
||
'titleEn': 'Privacy Policy',
|
||
'subtitle': '我们如何收集、使用、存储和保护您的个人信息',
|
||
'subtitleEn': 'How we collect, use, store and protect your personal information',
|
||
},
|
||
'userServiceAgreement': {
|
||
'filename': 'user-service-agreement.html',
|
||
'icon': '📋',
|
||
'title': '用户服务协议',
|
||
'titleEn': 'User Service Agreement',
|
||
'subtitle': '使用闲言APP的服务条款与规则',
|
||
'subtitleEn': 'Terms and rules for using Xianyan APP',
|
||
},
|
||
'accountAgreement': {
|
||
'filename': 'account-agreement.html',
|
||
'icon': '👤',
|
||
'title': '账号使用协议',
|
||
'titleEn': 'Account Agreement',
|
||
'subtitle': '账号注册、安全、注销等相关规定',
|
||
'subtitleEn': 'Rules for account registration, security, and deletion',
|
||
},
|
||
'memberBenefits': {
|
||
'filename': 'member-benefits.html',
|
||
'icon': '👑',
|
||
'title': '会员权益说明',
|
||
'titleEn': 'Member Benefits',
|
||
'subtitle': '会员等级、权益与使用说明',
|
||
'subtitleEn': 'Membership levels, benefits and usage instructions',
|
||
},
|
||
'disclaimer': {
|
||
'filename': 'disclaimer.html',
|
||
'icon': '⚖️',
|
||
'title': '免责声明及内容版权归属',
|
||
'titleEn': 'Disclaimer & Copyright',
|
||
'subtitle': '免责条款、内容版权与知识产权声明',
|
||
'subtitleEn': 'Disclaimers, content copyright and intellectual property',
|
||
},
|
||
'childrenPrivacy': {
|
||
'filename': 'children-privacy.html',
|
||
'icon': '👶',
|
||
'title': '儿童隐私政策',
|
||
'titleEn': 'Children\'s Privacy Policy',
|
||
'subtitle': '儿童及未成年人个人信息保护规则',
|
||
'subtitleEn': 'Personal information protection rules for children and minors',
|
||
},
|
||
'permissionUsage': {
|
||
'filename': 'permission-usage.html',
|
||
'icon': '🔧',
|
||
'title': '软件权限使用说明',
|
||
'titleEn': 'Permission Usage',
|
||
'subtitle': '软件所需权限及使用目的说明',
|
||
'subtitleEn': 'App permissions and their usage purposes',
|
||
},
|
||
'appIntroduction': {
|
||
'filename': 'app-introduction.html',
|
||
'icon': '✨',
|
||
'title': '软件介绍',
|
||
'titleEn': 'App Introduction',
|
||
'subtitle': '闲言APP功能介绍与平台支持',
|
||
'subtitleEn': 'Xianyan APP features and platform support',
|
||
},
|
||
'beginnerGuide': {
|
||
'filename': 'beginner-guide.html',
|
||
'icon': '📖',
|
||
'title': '新手指引',
|
||
'titleEn': 'Beginner Guide',
|
||
'subtitle': '功能导览与操作指引',
|
||
'subtitleEn': 'Feature overview and operation guide',
|
||
},
|
||
'devTeam': {
|
||
'filename': 'dev-team.html',
|
||
'icon': '💻',
|
||
'title': '开发团队',
|
||
'titleEn': 'Development Team',
|
||
'subtitle': '开发团队信息与联系方式',
|
||
'subtitleEn': 'Development team information and contact',
|
||
},
|
||
}
|
||
|
||
|
||
def extract_agreements(dart_path):
|
||
"""从Dart文件提取中英文协议内容"""
|
||
with open(dart_path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
agreements_zh = {}
|
||
agreements_en = {}
|
||
|
||
pattern = r"static const (\w+)Content = '''\n(.*?)\n''';"
|
||
for match in re.finditer(pattern, content, re.DOTALL):
|
||
key = match.group(1)
|
||
text = match.group(2)
|
||
agreements_zh[key] = text
|
||
|
||
pattern_en = r"static const (\w+)ContentEn = '''\n(.*?)\n''';"
|
||
for match in re.finditer(pattern_en, content, re.DOTALL):
|
||
key = match.group(1)
|
||
text = match.group(2)
|
||
agreements_en[key] = text
|
||
|
||
return agreements_zh, agreements_en
|
||
|
||
|
||
def markdown_to_html(text, is_en=False):
|
||
"""将简易Markdown转换为HTML"""
|
||
lines = text.split('\n')
|
||
html_lines = []
|
||
in_table = False
|
||
in_ul = False
|
||
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
|
||
if not stripped:
|
||
if in_ul:
|
||
html_lines.append('</ul>')
|
||
in_ul = False
|
||
if in_table:
|
||
html_lines.append('</tbody></table>')
|
||
in_table = False
|
||
continue
|
||
|
||
if stripped.startswith('|') and '|' in stripped[1:]:
|
||
cells = [c.strip() for c in stripped.split('|')[1:-1]]
|
||
if all(set(c) <= set('-: ') for c in cells):
|
||
continue
|
||
if not in_table:
|
||
if in_ul:
|
||
html_lines.append('</ul>')
|
||
in_ul = False
|
||
html_lines.append('<table><thead><tr>')
|
||
for c in cells:
|
||
html_lines.append(f'<th>{c}</th>')
|
||
html_lines.append('</tr></thead><tbody>')
|
||
in_table = True
|
||
else:
|
||
html_lines.append('<tr>')
|
||
for c in cells:
|
||
html_lines.append(f'<td>{c}</td>')
|
||
html_lines.append('</tr>')
|
||
continue
|
||
else:
|
||
if in_table:
|
||
html_lines.append('</tbody></table>')
|
||
in_table = False
|
||
|
||
if stripped.startswith('•'):
|
||
if not in_ul:
|
||
html_lines.append('<ul>')
|
||
in_ul = True
|
||
item_text = stripped[1:].strip()
|
||
item_text = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', item_text)
|
||
html_lines.append(f'<li>{item_text}</li>')
|
||
if '2020SR0421982' in stripped:
|
||
html_lines.append('</ul>')
|
||
in_ul = False
|
||
html_lines.append('<div class="copyright-cert">')
|
||
html_lines.append('<img src="rz.png" alt="软件著作权登记证书" class="cert-image" />')
|
||
cert_caption = 'Software Copyright Registration Certificate' if is_en else '软件著作权登记证书'
|
||
html_lines.append(f'<p class="cert-caption">{cert_caption}</p>')
|
||
html_lines.append('</div>')
|
||
continue
|
||
elif stripped.startswith('-'):
|
||
if not in_ul:
|
||
html_lines.append('<ul>')
|
||
in_ul = True
|
||
item_text = stripped[1:].strip()
|
||
item_text = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', item_text)
|
||
html_lines.append(f'<li>{item_text}</li>')
|
||
continue
|
||
else:
|
||
if in_ul:
|
||
html_lines.append('</ul>')
|
||
in_ul = False
|
||
|
||
if stripped.startswith('**') and stripped.endswith('**'):
|
||
heading = stripped[2:-2]
|
||
html_lines.append(f'<h1>{heading}</h1>')
|
||
elif re.match(r'^[一二三四五六七八九十零]+[点五]*、', stripped):
|
||
heading = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
|
||
html_lines.append(f'<h2>{heading}</h2>')
|
||
elif re.match(r'^[IVX]+\.\s', stripped):
|
||
heading = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
|
||
html_lines.append(f'<h2>{heading}</h2>')
|
||
elif re.match(r'^\d+\.\d+', stripped):
|
||
heading = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
|
||
html_lines.append(f'<h3>{heading}</h3>')
|
||
elif stripped.startswith('说明:') or stripped.startswith('说明:'):
|
||
text_content = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
|
||
html_lines.append(f'<p class="note">{text_content}</p>')
|
||
elif stripped.startswith('Note:') or stripped.startswith('Note:'):
|
||
text_content = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
|
||
html_lines.append(f'<p class="note">{text_content}</p>')
|
||
else:
|
||
text_content = re.sub(r'【(.+?)】', r'<span class="highlight">\1</span>', stripped)
|
||
text_content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text_content)
|
||
html_lines.append(f'<p>{text_content}</p>')
|
||
|
||
if in_ul:
|
||
html_lines.append('</ul>')
|
||
if in_table:
|
||
html_lines.append('</tbody></table>')
|
||
|
||
return '\n'.join(html_lines)
|
||
|
||
|
||
def generate_html(info, content_html_zh, content_html_en):
|
||
"""生成带语言切换的双语HTML页面"""
|
||
icon = info['icon']
|
||
title_zh = info['title']
|
||
title_en = info['titleEn']
|
||
subtitle_zh = info['subtitle']
|
||
subtitle_en = info['subtitleEn']
|
||
|
||
return f'''<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{title_zh} - 闲言APP</title>
|
||
<style>
|
||
:root {{
|
||
--primary: #6C5CE7;
|
||
--primary-light: #A29BFE;
|
||
--primary-dark: #5A4BD1;
|
||
--bg: #F2F2F7;
|
||
--bg-card: #FFFFFF;
|
||
--text: #1C1C1E;
|
||
--text-secondary: #8E8E93;
|
||
--text-tertiary: #AEAEB2;
|
||
--border: #E5E5EA;
|
||
--highlight-bg: rgba(108, 92, 231, 0.08);
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||
--radius-sm: 8px;
|
||
--radius-md: 12px;
|
||
--radius-lg: 16px;
|
||
--radius-xl: 20px;
|
||
--font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
}}
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
body {{
|
||
font-family: var(--font-family);
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.8;
|
||
-webkit-font-smoothing: antialiased;
|
||
}}
|
||
.lang-switch {{
|
||
position: fixed;
|
||
top: 16px;
|
||
right: 16px;
|
||
z-index: 100;
|
||
display: flex;
|
||
background: rgba(255,255,255,0.85);
|
||
backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
border-radius: 20px;
|
||
padding: 3px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||
border: 0.5px solid rgba(108, 92, 231, 0.15);
|
||
}}
|
||
.lang-btn {{
|
||
padding: 6px 14px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border-radius: 17px;
|
||
transition: all 0.25s ease;
|
||
font-family: var(--font-family);
|
||
}}
|
||
.lang-btn.active {{
|
||
background: var(--primary);
|
||
color: #FFF;
|
||
box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3);
|
||
}}
|
||
.lang-btn:hover:not(.active) {{
|
||
background: rgba(108, 92, 231, 0.08);
|
||
color: var(--primary);
|
||
}}
|
||
.header {{
|
||
background: linear-gradient(135deg, #6C5CE7 0%, #5A4BD1 50%, #4A3DB5 100%);
|
||
padding: 48px 20px 44px;
|
||
text-align: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}}
|
||
.header::before {{
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -50%;
|
||
width: 200%;
|
||
height: 200%;
|
||
background: radial-gradient(circle at 30% 50%, rgba(255,255,255,0.1) 0%, transparent 50%);
|
||
pointer-events: none;
|
||
}}
|
||
.header::after {{
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 32px;
|
||
background: var(--bg);
|
||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||
}}
|
||
.header-icon {{ font-size: 40px; margin-bottom: 10px; display: block; }}
|
||
.header h1 {{ color: #FFF; font-size: 24px; font-weight: 700; letter-spacing: -0.3px; margin-bottom: 4px; }}
|
||
.header p {{ color: rgba(255,255,255,0.75); font-size: 14px; }}
|
||
.container {{ max-width: 800px; margin: 0 auto; padding: 20px 16px 40px; }}
|
||
.content-card {{
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-lg);
|
||
padding: 28px 24px;
|
||
margin-bottom: 20px;
|
||
box-shadow: var(--shadow-sm);
|
||
}}
|
||
h1 {{
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin: 28px 0 12px;
|
||
}}
|
||
h2 {{
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin: 32px 0 16px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 2px solid var(--primary);
|
||
display: inline-block;
|
||
}}
|
||
h3 {{
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
margin: 24px 0 12px;
|
||
}}
|
||
p {{
|
||
font-size: 15px;
|
||
color: var(--text);
|
||
margin-bottom: 12px;
|
||
line-height: 1.8;
|
||
}}
|
||
p.note {{
|
||
background: rgba(108, 92, 231, 0.06);
|
||
border-left: 3px solid var(--primary);
|
||
padding: 12px 16px;
|
||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||
margin: 12px 0;
|
||
}}
|
||
.highlight {{
|
||
color: var(--primary);
|
||
font-weight: 600;
|
||
background: var(--highlight-bg);
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
}}
|
||
.copyright-cert {{
|
||
margin: 20px 0;
|
||
text-align: center;
|
||
padding: 20px;
|
||
background: rgba(108, 92, 231, 0.04);
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid rgba(108, 92, 231, 0.12);
|
||
}}
|
||
.cert-image {{
|
||
max-width: 100%;
|
||
max-height: 500px;
|
||
border-radius: var(--radius-sm);
|
||
box-shadow: var(--shadow-md);
|
||
}}
|
||
.cert-caption {{
|
||
margin-top: 12px;
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
font-weight: 500;
|
||
}}
|
||
ul {{
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0 0 16px;
|
||
}}
|
||
li {{
|
||
font-size: 15px;
|
||
color: var(--text);
|
||
padding: 6px 0 6px 20px;
|
||
position: relative;
|
||
line-height: 1.7;
|
||
}}
|
||
li::before {{
|
||
content: '\\2022';
|
||
color: var(--primary);
|
||
font-weight: 700;
|
||
position: absolute;
|
||
left: 4px;
|
||
top: 6px;
|
||
}}
|
||
table {{
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 16px 0;
|
||
font-size: 14px;
|
||
}}
|
||
th {{
|
||
background: var(--primary);
|
||
color: #FFF;
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}}
|
||
td {{
|
||
padding: 10px 12px;
|
||
border-bottom: 0.5px solid var(--border);
|
||
line-height: 1.6;
|
||
}}
|
||
tr:hover td {{
|
||
background: rgba(108, 92, 231, 0.04);
|
||
}}
|
||
.back-link {{
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
padding: 12px 0;
|
||
transition: opacity 0.2s;
|
||
}}
|
||
.back-link:hover {{ opacity: 0.7; }}
|
||
.footer {{
|
||
text-align: center;
|
||
padding: 24px 20px 40px;
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
border-top: 0.5px solid var(--border);
|
||
margin-top: 20px;
|
||
}}
|
||
.footer .company {{ font-weight: 500; color: var(--text); margin-bottom: 4px; }}
|
||
@media (max-width: 640px) {{
|
||
.lang-switch {{ top: 10px; right: 10px; }}
|
||
.lang-btn {{ padding: 5px 10px; font-size: 12px; }}
|
||
.header {{ padding: 40px 16px 36px; }}
|
||
.header h1 {{ font-size: 20px; }}
|
||
.container {{ padding: 16px 12px 32px; }}
|
||
.content-card {{ padding: 20px 16px; }}
|
||
h2 {{ font-size: 18px; }}
|
||
h3 {{ font-size: 16px; }}
|
||
p, li {{ font-size: 14px; }}
|
||
table {{ font-size: 12px; }}
|
||
th, td {{ padding: 8px 6px; }}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="lang-switch">
|
||
<button class="lang-btn active" onclick="switchLang('zh')" id="btn-zh">中文</button>
|
||
<button class="lang-btn" onclick="switchLang('en')" id="btn-en">EN</button>
|
||
</div>
|
||
|
||
<div class="header">
|
||
<span class="header-icon">{icon}</span>
|
||
<h1 id="header-title">{title_zh}</h1>
|
||
<p id="header-subtitle">{subtitle_zh}</p>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="content-card">
|
||
<div id="content-zh" class="lang-content">
|
||
{content_html_zh}
|
||
</div>
|
||
<div id="content-en" class="lang-content" style="display:none;">
|
||
{content_html_en}
|
||
</div>
|
||
</div>
|
||
|
||
<a href="index.html" class="back-link" id="back-link">← 返回协议列表</a>
|
||
</div>
|
||
|
||
<div class="footer" id="footer">
|
||
<div class="company" id="footer-company">弥勒市朋普镇微风暴网络科技工作室</div>
|
||
<div id="footer-contact">📧 21981550@qq.com | 📍 云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)</div>
|
||
<div style="margin-top: 8px;" id="footer-credit">统一社会信用代码:92532526MA6PCX153W</div>
|
||
<div style="margin-top: 4px; color: var(--text-tertiary);">© 2026 Xianyan. All rights reserved.</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DATA = {{
|
||
zh: {{
|
||
title: '{title_zh}',
|
||
subtitle: '{subtitle_zh}',
|
||
backLink: '← 返回协议列表',
|
||
company: '弥勒市朋普镇微风暴网络科技工作室',
|
||
contact: '📧 21981550@qq.com | 📍 云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)',
|
||
credit: '统一社会信用代码:92532526MA6PCX153W'
|
||
}},
|
||
en: {{
|
||
title: '{title_en}',
|
||
subtitle: '{subtitle_en}',
|
||
backLink: '← Back to Agreement List',
|
||
company: 'Mile City Pengpu Town Weifengbao Network Technology Studio',
|
||
contact: '📧 21981550@qq.com | 📍 Dianchi Resort, Xishan District, Kunming, Yunnan, China',
|
||
credit: 'Unified Social Credit Code: 92532526MA6PCX153W'
|
||
}}
|
||
}};
|
||
|
||
function switchLang(lang) {{
|
||
document.getElementById('content-zh').style.display = lang === 'zh' ? 'block' : 'none';
|
||
document.getElementById('content-en').style.display = lang === 'en' ? 'block' : 'none';
|
||
document.getElementById('header-title').textContent = DATA[lang].title;
|
||
document.getElementById('header-subtitle').textContent = DATA[lang].subtitle;
|
||
document.getElementById('back-link').textContent = DATA[lang].backLink;
|
||
document.getElementById('footer-company').textContent = DATA[lang].company;
|
||
document.getElementById('footer-contact').innerHTML = DATA[lang].contact;
|
||
document.getElementById('footer-credit').textContent = DATA[lang].credit;
|
||
document.getElementById('btn-zh').className = 'lang-btn' + (lang === 'zh' ? ' active' : '');
|
||
document.getElementById('btn-en').className = 'lang-btn' + (lang === 'en' ? ' active' : '');
|
||
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
||
const url = new URL(window.location);
|
||
if (lang === 'en') {{
|
||
url.searchParams.set('lang', 'en');
|
||
}} else {{
|
||
url.searchParams.delete('lang');
|
||
}}
|
||
history.replaceState(null, '', url);
|
||
}}
|
||
|
||
(function() {{
|
||
const params = new URLSearchParams(window.location.search);
|
||
const lang = params.get('lang');
|
||
if (lang === 'en') switchLang('en');
|
||
}})();
|
||
</script>
|
||
</body>
|
||
</html>'''
|
||
|
||
|
||
def generate_index_html():
|
||
"""生成带语言切换的协议列表首页"""
|
||
keys = list(AGREEMENT_MAP.keys())
|
||
sections = [
|
||
{'icon': '🔒', 'titleZh': '隐私与安全', 'titleEn': 'Privacy & Security', 'range': range(0, 3)},
|
||
{'icon': '📋', 'titleZh': '服务条款', 'titleEn': 'Service Terms', 'range': range(3, 6)},
|
||
{'icon': '⚖️', 'titleZh': '内容与版权', 'titleEn': 'Content & Copyright', 'range': range(6, 7)},
|
||
{'icon': '💡', 'titleZh': '了解闲言', 'titleEn': 'About Xianyan', 'range': range(7, 10)},
|
||
]
|
||
|
||
def build_items(key_list, lang='zh'):
|
||
html = ''
|
||
for key in key_list:
|
||
info = AGREEMENT_MAP[key]
|
||
if lang == 'zh':
|
||
title = info['title']
|
||
subtitle = info['subtitle']
|
||
href = info['filename']
|
||
else:
|
||
title = info['titleEn']
|
||
subtitle = info['subtitleEn']
|
||
href = f"{info['filename']}?lang=en"
|
||
html += f'''
|
||
<a href="{href}" class="agreement-item">
|
||
<span class="item-icon">{info['icon']}</span>
|
||
<div class="item-text">
|
||
<div class="item-title">{title}</div>
|
||
<div class="item-subtitle">{subtitle}</div>
|
||
</div>
|
||
<span class="item-arrow">›</span>
|
||
</a>'''
|
||
return html
|
||
|
||
def build_sections(lang='zh'):
|
||
html = ''
|
||
for sec in sections:
|
||
section_title = sec['titleZh'] if lang == 'zh' else sec['titleEn']
|
||
section_keys = [keys[i] for i in sec['range'] if i < len(keys)]
|
||
html += f'''
|
||
<div class="section-title">{sec['icon']} {section_title}</div>
|
||
<div class="agreement-list">
|
||
{build_items(section_keys, lang)}
|
||
</div>'''
|
||
return html
|
||
|
||
return f'''<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>软件协议 - 闲言APP</title>
|
||
<style>
|
||
:root {{
|
||
--primary: #6C5CE7;
|
||
--primary-light: #A29BFE;
|
||
--primary-dark: #5A4BD1;
|
||
--bg: #F2F2F7;
|
||
--bg-card: #FFFFFF;
|
||
--text: #1C1C1E;
|
||
--text-secondary: #8E8E93;
|
||
--text-tertiary: #AEAEB2;
|
||
--border: #E5E5EA;
|
||
--highlight-bg: rgba(108, 92, 231, 0.08);
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.06);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||
--radius-sm: 8px;
|
||
--radius-md: 12px;
|
||
--radius-lg: 16px;
|
||
--radius-xl: 20px;
|
||
--font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
}}
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
body {{
|
||
font-family: var(--font-family);
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.8;
|
||
-webkit-font-smoothing: antialiased;
|
||
}}
|
||
.lang-switch {{
|
||
position: fixed;
|
||
top: 16px;
|
||
right: 16px;
|
||
z-index: 100;
|
||
display: flex;
|
||
background: rgba(255,255,255,0.85);
|
||
backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
border-radius: 20px;
|
||
padding: 3px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||
border: 0.5px solid rgba(108, 92, 231, 0.15);
|
||
}}
|
||
.lang-btn {{
|
||
padding: 6px 14px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border-radius: 17px;
|
||
transition: all 0.25s ease;
|
||
font-family: var(--font-family);
|
||
}}
|
||
.lang-btn.active {{
|
||
background: var(--primary);
|
||
color: #FFF;
|
||
box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3);
|
||
}}
|
||
.lang-btn:hover:not(.active) {{
|
||
background: rgba(108, 92, 231, 0.08);
|
||
color: var(--primary);
|
||
}}
|
||
.header {{
|
||
background: linear-gradient(135deg, #6C5CE7 0%, #5A4BD1 50%, #4A3DB5 100%);
|
||
padding: 48px 20px 44px;
|
||
text-align: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}}
|
||
.header::before {{
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -50%;
|
||
width: 200%;
|
||
height: 200%;
|
||
background: radial-gradient(circle at 30% 50%, rgba(255,255,255,0.1) 0%, transparent 50%);
|
||
pointer-events: none;
|
||
}}
|
||
.header::after {{
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 32px;
|
||
background: var(--bg);
|
||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||
}}
|
||
.header-icon {{ font-size: 40px; margin-bottom: 10px; display: block; }}
|
||
.header h1 {{ color: #FFF; font-size: 24px; font-weight: 700; letter-spacing: -0.3px; margin-bottom: 4px; }}
|
||
.header p {{ color: rgba(255,255,255,0.75); font-size: 14px; }}
|
||
.container {{ max-width: 800px; margin: 0 auto; padding: 20px 16px 40px; }}
|
||
.section-title {{
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
padding: 16px 4px 8px;
|
||
}}
|
||
.agreement-list {{
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-lg);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-sm);
|
||
margin-bottom: 16px;
|
||
}}
|
||
.agreement-item {{
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 14px 16px;
|
||
text-decoration: none;
|
||
color: var(--text);
|
||
transition: background 0.15s;
|
||
border-bottom: 0.5px solid var(--border);
|
||
}}
|
||
.agreement-item:last-child {{ border-bottom: none; }}
|
||
.agreement-item:hover {{ background: rgba(108, 92, 231, 0.04); }}
|
||
.item-icon {{ font-size: 28px; margin-right: 14px; flex-shrink: 0; }}
|
||
.item-text {{ flex: 1; min-width: 0; }}
|
||
.item-title {{ font-size: 16px; font-weight: 600; color: var(--text); }}
|
||
.item-subtitle {{ font-size: 13px; color: var(--text-secondary); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
|
||
.item-arrow {{ font-size: 20px; color: var(--text-tertiary); margin-left: 8px; }}
|
||
.footer {{
|
||
text-align: center;
|
||
padding: 24px 20px 40px;
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
border-top: 0.5px solid var(--border);
|
||
margin-top: 20px;
|
||
}}
|
||
.footer .company {{ font-weight: 500; color: var(--text); margin-bottom: 4px; }}
|
||
@media (max-width: 640px) {{
|
||
.lang-switch {{ top: 10px; right: 10px; }}
|
||
.lang-btn {{ padding: 5px 10px; font-size: 12px; }}
|
||
.header {{ padding: 40px 16px 36px; }}
|
||
.header h1 {{ font-size: 20px; }}
|
||
.container {{ padding: 16px 12px 32px; }}
|
||
.agreement-item {{ padding: 12px 14px; }}
|
||
.item-icon {{ font-size: 24px; margin-right: 12px; }}
|
||
.item-title {{ font-size: 15px; }}
|
||
.item-subtitle {{ font-size: 12px; }}
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="lang-switch">
|
||
<button class="lang-btn active" onclick="switchLang('zh')" id="btn-zh">中文</button>
|
||
<button class="lang-btn" onclick="switchLang('en')" id="btn-en">EN</button>
|
||
</div>
|
||
|
||
<div class="header">
|
||
<span class="header-icon">📋</span>
|
||
<h1 id="header-title">软件协议与政策</h1>
|
||
<p id="header-subtitle">请仔细阅读以下协议与政策,了解您的权利与义务</p>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div id="content-zh" class="lang-content">
|
||
{build_sections('zh')}
|
||
</div>
|
||
<div id="content-en" class="lang-content" style="display:none;">
|
||
{build_sections('en')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer" id="footer">
|
||
<div class="company" id="footer-company">弥勒市朋普镇微风暴网络科技工作室</div>
|
||
<div id="footer-contact">📧 21981550@qq.com | 📍 云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)</div>
|
||
<div style="margin-top: 8px;" id="footer-credit">统一社会信用代码:92532526MA6PCX153W</div>
|
||
<div style="margin-top: 4px; color: var(--text-tertiary);">© 2026 Xianyan. All rights reserved.</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DATA = {{
|
||
zh: {{
|
||
title: '软件协议与政策',
|
||
subtitle: '请仔细阅读以下协议与政策,了解您的权利与义务',
|
||
company: '弥勒市朋普镇微风暴网络科技工作室',
|
||
contact: '📧 21981550@qq.com | 📍 云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)',
|
||
credit: '统一社会信用代码:92532526MA6PCX153W'
|
||
}},
|
||
en: {{
|
||
title: 'Agreements & Policies',
|
||
subtitle: 'Please read the following agreements and policies carefully',
|
||
company: 'Mile City Pengpu Town Weifengbao Network Technology Studio',
|
||
contact: '📧 21981550@qq.com | 📍 Dianchi Resort, Xishan District, Kunming, Yunnan, China',
|
||
credit: 'Unified Social Credit Code: 92532526MA6PCX153W'
|
||
}}
|
||
}};
|
||
|
||
function switchLang(lang) {{
|
||
document.getElementById('content-zh').style.display = lang === 'zh' ? 'block' : 'none';
|
||
document.getElementById('content-en').style.display = lang === 'en' ? 'block' : 'none';
|
||
document.getElementById('header-title').textContent = DATA[lang].title;
|
||
document.getElementById('header-subtitle').textContent = DATA[lang].subtitle;
|
||
document.getElementById('footer-company').textContent = DATA[lang].company;
|
||
document.getElementById('footer-contact').innerHTML = DATA[lang].contact;
|
||
document.getElementById('footer-credit').textContent = DATA[lang].credit;
|
||
document.getElementById('btn-zh').className = 'lang-btn' + (lang === 'zh' ? ' active' : '');
|
||
document.getElementById('btn-en').className = 'lang-btn' + (lang === 'en' ? ' active' : '');
|
||
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
||
const url = new URL(window.location);
|
||
if (lang === 'en') {{
|
||
url.searchParams.set('lang', 'en');
|
||
}} else {{
|
||
url.searchParams.delete('lang');
|
||
}}
|
||
history.replaceState(null, '', url);
|
||
}}
|
||
|
||
(function() {{
|
||
const params = new URLSearchParams(window.location.search);
|
||
const lang = params.get('lang');
|
||
if (lang === 'en') switchLang('en');
|
||
}})();
|
||
</script>
|
||
</body>
|
||
</html>'''
|
||
|
||
|
||
def main():
|
||
print('=' * 60)
|
||
print('闲言APP 协议HTML生成与上传工具 (双语版)')
|
||
print('=' * 60)
|
||
|
||
print('\n[1/3] 从Dart文件提取中英文协议内容...')
|
||
agreements_zh, agreements_en = extract_agreements(DART_FILE)
|
||
print(f' 中文协议: {len(agreements_zh)} 个')
|
||
print(f' 英文协议: {len(agreements_en)} 个')
|
||
|
||
print('\n[2/3] 生成双语HTML文件...')
|
||
for key, info in AGREEMENT_MAP.items():
|
||
content_zh = agreements_zh.get(key)
|
||
content_en = agreements_en.get(key)
|
||
|
||
if not content_zh:
|
||
print(f' [SKIP] {key} 中文内容未找到')
|
||
continue
|
||
|
||
content_html_zh = markdown_to_html(content_zh, is_en=False)
|
||
content_html_en = markdown_to_html(content_en, is_en=True) if content_en else '<p style="color:var(--text-secondary);">English version coming soon...</p>'
|
||
|
||
html = generate_html(info, content_html_zh, content_html_en)
|
||
|
||
filepath = os.path.join(BASE_DIR, info['filename'])
|
||
with open(filepath, 'w', encoding='utf-8') as f:
|
||
f.write(html)
|
||
status = '✅' if content_en else '⚠️ (EN缺失)'
|
||
print(f' [OK] {info["filename"]} {status}')
|
||
|
||
index_html = generate_index_html()
|
||
index_path = os.path.join(BASE_DIR, 'index.html')
|
||
with open(index_path, 'w', encoding='utf-8') as f:
|
||
f.write(index_html)
|
||
print(f' [OK] index.html')
|
||
|
||
print('\n[3/3] 上传到服务器...')
|
||
ssh = paramiko.SSHClient()
|
||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||
ssh.connect(HOST, PORT, USER, PASS)
|
||
sftp = ssh.open_sftp()
|
||
|
||
for key, info in AGREEMENT_MAP.items():
|
||
local = os.path.join(BASE_DIR, info['filename'])
|
||
remote = REMOTE_BASE + info['filename']
|
||
if not os.path.exists(local):
|
||
print(f' [SKIP] {info["filename"]} 本地文件不存在')
|
||
continue
|
||
print(f' 上传 {info["filename"]}...')
|
||
sftp.put(local, remote)
|
||
sftp.chmod(remote, 0o644)
|
||
print(f' [OK] {info["filename"]}')
|
||
|
||
index_local = os.path.join(BASE_DIR, 'index.html')
|
||
index_remote = REMOTE_BASE + 'index.html'
|
||
if os.path.exists(index_local):
|
||
print(' 上传 index.html...')
|
||
sftp.put(index_local, index_remote)
|
||
sftp.chmod(index_remote, 0o644)
|
||
print(' [OK] index.html')
|
||
|
||
sftp.close()
|
||
ssh.close()
|
||
|
||
print('\n' + '=' * 60)
|
||
print('完成!所有双语协议HTML已生成并上传到服务器')
|
||
print(f'中文版: https://tools.wktyl.com/agreements/index.html')
|
||
print(f'英文版: https://tools.wktyl.com/agreements/index.html?lang=en')
|
||
print('=' * 60)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|