diff --git a/admin-frontend/index.html b/admin-frontend/index.html index 1339ae8..443dc49 100644 --- a/admin-frontend/index.html +++ b/admin-frontend/index.html @@ -2,7 +2,36 @@ - + + + + + + + + + + + + + + + + + + + + TradeMate 管理后台 diff --git a/admin-frontend/public/robots.txt b/admin-frontend/public/robots.txt new file mode 100644 index 0000000..349f334 --- /dev/null +++ b/admin-frontend/public/robots.txt @@ -0,0 +1,7 @@ +User-agent: * +Allow: / +Disallow: /api/ +Disallow: /static/ +Disallow: /assets/ + +Sitemap: https://trade.yuzhiran.com/sitemap.xml \ No newline at end of file diff --git a/tests/browser_automation.py b/tests/browser_automation.py new file mode 100644 index 0000000..b40c959 --- /dev/null +++ b/tests/browser_automation.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +TradeMate 前端 SEO 与浏览器自动化测试脚本 +使用 requests + BeautifulSoup 进行 SEO 审计 +""" + +import json +import re +import sys +from pathlib import Path +from urllib.parse import urljoin, urlparse + +try: + import requests + from bs4 import BeautifulSoup + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + print("⚠️ requests/bs4 未安装,跳过网络测试") + +BASE_URL = "https://trade.yuzhiran.com" +TEST_RESULTS = [] + +def run_test(name: str, test_func): + """运行单个测试并记录结果""" + try: + result = test_func() + TEST_RESULTS.append({"name": name, "status": "PASS", "detail": result}) + print(f"✅ {name}") + except Exception as e: + TEST_RESULTS.append({"name": name, "status": "FAIL", "detail": str(e)}) + print(f"❌ {name}: {e}") + +def get_page(url: str, timeout: int = 15) -> tuple: + """获取页面内容""" + if not HAS_REQUESTS: + raise RuntimeError("requests not available") + response = requests.get(url, timeout=timeout, headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }) + response.raise_for_status() + return response + +def parse_html(html: str) -> BeautifulSoup: + """解析 HTML""" + return BeautifulSoup(html, 'html.parser', from_encoding='utf-8') + +def test_homepage_seo(): + """测试首页 SEO 优化""" + response = get_page(BASE_URL) + soup = parse_html(response.text) + + # 检查 title + title = soup.title.string if soup.title else "" + assert "TradeMate" in title, f"Title should contain TradeMate, got: {title}" + + # 检查 meta description + desc_tag = soup.find('meta', attrs={'name': 'description'}) + desc = desc_tag.get('content', '') if desc_tag else "" + assert len(desc) > 50, f"Meta description too short: {desc}" + + # 检查 viewport + viewport_tag = soup.find('meta', attrs={'name': 'viewport'}) + viewport = viewport_tag.get('content', '') if viewport_tag else "" + assert "width=device-width" in viewport, f"Viewport missing: {viewport}" + + # 检查 canonical + canonical_tag = soup.find('link', attrs={'rel': 'canonical'}) + canonical = canonical_tag.get('href', '') if canonical_tag else "" + assert canonical == BASE_URL + "/", f"Canonical incorrect: {canonical}" + + # 检查 Open Graph + og_title_tag = soup.find('meta', attrs={'property': 'og:title'}) + og_title = og_title_tag.get('content', '') if og_title_tag else "" + assert "TradeMate" in og_title, f"OG title missing: {og_title}" + + # 检查结构化数据 + ld_scripts = soup.find_all('script', attrs={'type': 'application/ld+json'}) + assert len(ld_scripts) > 0, "No structured data found" + + return { + "title": title, + "description": desc[:100] + "...", + "canonical": canonical, + "og_title": og_title, + "structured_data_count": len(ld_scripts) + } + +def test_homepage_performance(): + """测试首页性能(简化版)""" + response = get_page(BASE_URL) + + # 检查页面大小 + html_size = len(response.text.encode('utf-8')) + assert html_size < 500000, f"HTML too large: {html_size} bytes" + + # 检查 HTTP 状态 + assert response.status_code == 200, f"Status code: {response.status_code}" + + # 检查压缩 + content_encoding = response.headers.get('Content-Encoding', '') + + return { + "html_size": html_size, + "status_code": response.status_code, + "content_encoding": content_encoding, + "load_time_ms": response.elapsed.total_seconds() * 1000 + } + +def test_workspace_seo(): + """测试工作台 SEO""" + response = get_page(BASE_URL + "/workspace/") + soup = parse_html(response.text) + + title = soup.title.string if soup.title else "" + assert "工作台" in title or "TradeMate" in title, f"Workspace title incorrect: {title}" + + desc_tag = soup.find('meta', attrs={'name': 'description'}) + desc = desc_tag.get('content', '') if desc_tag else "" + assert len(desc) > 30, f"Workspace description too short: {desc}" + + # 检查 robots + robots_tag = soup.find('meta', attrs={'name': 'robots'}) + robots = robots_tag.get('content', '') if robots_tag else "" + assert "noindex" in robots, f"Workspace should have noindex: {robots}" + + return {"title": title, "description": desc[:100] + "...", "robots": robots} + +def test_admin_seo(): + """测试管理后台 SEO""" + response = get_page(BASE_URL + "/admin/") + soup = parse_html(response.text) + + title = soup.title.string if soup.title else "" + assert "管理后台" in title or "TradeMate" in title, f"Admin title incorrect: {title}" + + # 检查 robots + robots_tag = soup.find('meta', attrs={'name': 'robots'}) + robots = robots_tag.get('content', '') if robots_tag else "" + assert "noindex" in robots, f"Admin should have noindex: {robots}" + + return {"title": title, "robots": robots} + +def test_app_seo(): + """测试移动端 App SEO""" + response = get_page(BASE_URL + "/app/") + soup = parse_html(response.text) + + title = soup.title.string if soup.title else "" + # Accept both full title and short title + assert "TradeMate" in title or "外贸" in title or "小助手" in title, f"App title incorrect: {title}" + + desc_tag = soup.find('meta', attrs={'name': 'description'}) + desc = desc_tag.get('content', '') if desc_tag else "" + assert len(desc) > 30, f"App description too short: {desc}" + + # 检查 PWA meta + apple_capable_tag = soup.find('meta', attrs={'name': 'apple-mobile-web-app-capable'}) + apple_capable = apple_capable_tag.get('content', '') if apple_capable_tag else "" + assert apple_capable == "yes", f"Apple web app capable missing: {apple_capable}" + + # 检查 theme-color + theme_color_tag = soup.find('meta', attrs={'name': 'theme-color'}) + theme_color = theme_color_tag.get('content', '') if theme_color_tag else "" + assert theme_color, f"Theme color missing: {theme_color}" + + return {"title": title, "description": desc[:100] + "...", "apple_capable": apple_capable, "theme_color": theme_color} + +def test_robots_txt(): + """测试 robots.txt""" + response = get_page(BASE_URL + "/robots.txt") + assert response.status_code == 200, f"robots.txt returned {response.status_code}" + + content = response.text + assert "User-agent" in content, "robots.txt missing User-agent" + assert "Disallow" in content, "robots.txt missing Disallow" + assert "Sitemap" in content, "robots.txt missing Sitemap" + + return {"status": response.status_code, "has_user_agent": True, "has_disallow": True, "has_sitemap": True} + +def test_sitemap_xml(): + """测试 sitemap.xml""" + response = get_page(BASE_URL + "/sitemap.xml") + assert response.status_code == 200, f"sitemap.xml returned {response.status_code}" + + content = response.text + assert "" in content, "sitemap.xml missing url entries" + assert BASE_URL in content, "sitemap.xml missing base URL" + + return {"status": response.status_code, "has_urlset": True, "has_urls": True} + +def test_image_optimization(): + """测试图片优化""" + response = get_page(BASE_URL) + soup = parse_html(response.text) + + images = soup.find_all('img') + results = [] + + for img in images: + src = img.get('src', '') + alt = img.get('alt', '') + width = img.get('width') + height = img.get('height') + + result = {"src": src, "has_alt": bool(alt), "has_dimensions": bool(width or height)} + if src: + result["is_webp"] = src.endswith('.webp') + result["is_lazy"] = img.get('loading') == 'lazy' + results.append(result) + + # 检查是否有图片缺少 alt + missing_alt = [r for r in results if not r.get('has_alt')] + + return {"total_images": len(results), "missing_alt": len(missing_alt), "images": results[:5]} + +def test_links(): + """测试链接有效性""" + response = get_page(BASE_URL) + soup = parse_html(response.text) + + links = soup.find_all('a', href=True) + broken_links = [] + + for link in links: + href = link['href'] + if href.startswith('#') or href.startswith('mailto:'): + continue + + full_url = urljoin(BASE_URL, href) + try: + link_response = requests.get(full_url, timeout=5, allow_redirects=True) + if link_response.status_code >= 400: + broken_links.append({"href": href, "status": link_response.status_code}) + except: + broken_links.append({"href": href, "status": "timeout"}) + + return {"total_links": len(links), "broken_links": broken_links} + +def test_semantic_html(): + """测试语义化 HTML""" + response = get_page(BASE_URL) + soup = parse_html(response.text) + + # 检查语义化标签 + semantic_tags = ['header', 'nav', 'main', 'section', 'article', 'aside', 'footer'] + found_tags = [] + + for tag in semantic_tags: + if soup.find(tag): + found_tags.append(tag) + + # 检查 heading 层级 + headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) + h1_count = len(soup.find_all('h1')) + + return { + "semantic_tags_found": found_tags, + "heading_count": len(headings), + "h1_count": h1_count, + "has_main": 'main' in found_tags + } + +def test_mobile_meta(): + """测试移动端 meta 标签""" + response = get_page(BASE_URL + "/app/") + soup = parse_html(response.text) + + viewport_tag = soup.find('meta', attrs={'name': 'viewport'}) + viewport = viewport_tag.get('content', '') if viewport_tag else "" + + # 检查移动端相关 meta + mobile_meta = { + "viewport": viewport, + "apple_capable": soup.find('meta', attrs={'name': 'apple-mobile-web-app-capable'}) is not None, + "apple_status_bar": soup.find('meta', attrs={'name': 'apple-mobile-web-app-status-bar-style'}) is not None, + "mobile_capable": soup.find('meta', attrs={'name': 'mobile-web-app-capable'}) is not None, + "theme_color": soup.find('meta', attrs={'name': 'theme-color'}) is not None + } + + return mobile_meta + +def main(): + """主测试函数""" + print("=" * 60) + print("TradeMate 前端 SEO 与浏览器自动化测试") + print("=" * 60) + + if not HAS_REQUESTS: + print("\n❌ requests/bs4 未安装,无法运行测试") + print(" 请运行: pip install requests beautifulsoup4") + return 1 + + # 首页测试 + print("\n📄 首页测试") + run_test("首页 SEO 优化", test_homepage_seo) + run_test("首页性能", test_homepage_performance) + + # 子页面测试 + print("\n📊 子页面测试") + run_test("工作台 SEO", test_workspace_seo) + run_test("管理后台 SEO", test_admin_seo) + run_test("移动端 App SEO", test_app_seo) + + # SEO 文件测试 + print("\n🔍 SEO 文件测试") + run_test("robots.txt", test_robots_txt) + run_test("sitemap.xml", test_sitemap_xml) + + # 其他测试 + print("\n🖼️ 其他测试") + run_test("图片优化", test_image_optimization) + run_test("链接有效性", test_links) + run_test("语义化 HTML", test_semantic_html) + run_test("移动端 meta", test_mobile_meta) + + # 输出结果 + print("\n" + "=" * 60) + print("测试结果汇总") + print("=" * 60) + + passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS") + failed = sum(1 for r in TEST_RESULTS if r["status"] == "FAIL") + + for result in TEST_RESULTS: + status_icon = "✅" if result["status"] == "PASS" else "❌" + detail = result['detail'] + if isinstance(detail, dict): + detail = json.dumps(detail, ensure_ascii=False, indent=2) + print(f"{status_icon} {result['name']}") + if result['status'] == 'FAIL': + print(f" → {detail}") + + print(f"\n总计: {len(TEST_RESULTS)} 个测试, {passed} 通过, {failed} 失败") + + # 保存结果到 JSON + output_path = Path(__file__).parent / "test_results.json" + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(TEST_RESULTS, f, ensure_ascii=False, indent=2) + print(f"\n结果已保存到: {output_path}") + + return 0 if failed == 0 else 1 + +if __name__ == "__main__": + exit_code = main() + sys.exit(exit_code) \ No newline at end of file diff --git a/uni-app/index.html b/uni-app/index.html index cef7e67..bc1c9a8 100644 --- a/uni-app/index.html +++ b/uni-app/index.html @@ -2,10 +2,57 @@ - - 外贸小助手 - TradeMate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TradeMate 外贸小助手 - AI 驱动的外贸智能工作台
diff --git a/uni-app/public/robots.txt b/uni-app/public/robots.txt new file mode 100644 index 0000000..a9b85b9 --- /dev/null +++ b/uni-app/public/robots.txt @@ -0,0 +1,24 @@ +User-agent: * +Allow: / +Allow: /app/ +Allow: /workspace/ +Disallow: /admin/ +Disallow: /api/ +Disallow: /static/ +Disallow: /assets/ + +User-agent: Googlebot +Allow: / +Allow: /app/ +Allow: /workspace/ +Disallow: /admin/ +Disallow: /api/ + +User-agent: Baiduspider +Allow: / +Allow: /app/ +Allow: /workspace/ +Disallow: /admin/ +Disallow: /api/ + +Sitemap: https://trade.yuzhiran.com/sitemap.xml \ No newline at end of file diff --git a/uni-app/public/sitemap.xml b/uni-app/public/sitemap.xml new file mode 100644 index 0000000..5268493 --- /dev/null +++ b/uni-app/public/sitemap.xml @@ -0,0 +1,34 @@ + + + + https://trade.yuzhiran.com/ + 2026-06-29 + weekly + 1.0 + + + https://trade.yuzhiran.com/app/ + 2026-06-29 + daily + 0.9 + + + https://trade.yuzhiran.com/workspace/ + 2026-06-29 + daily + 0.8 + + + https://trade.yuzhiran.com/app/pages/agreement/privacy + 2026-06-29 + monthly + 0.5 + + + https://trade.yuzhiran.com/app/pages/agreement/terms + 2026-06-29 + monthly + 0.5 + + \ No newline at end of file diff --git a/user-frontend/index.html b/user-frontend/index.html index d2a039e..975ec00 100644 --- a/user-frontend/index.html +++ b/user-frontend/index.html @@ -2,7 +2,36 @@ - + + + + + + + + + + + + + + + + + + + + TradeMate 工作台 diff --git a/user-frontend/public/robots.txt b/user-frontend/public/robots.txt new file mode 100644 index 0000000..349f334 --- /dev/null +++ b/user-frontend/public/robots.txt @@ -0,0 +1,7 @@ +User-agent: * +Allow: / +Disallow: /api/ +Disallow: /static/ +Disallow: /assets/ + +Sitemap: https://trade.yuzhiran.com/sitemap.xml \ No newline at end of file