接口自动化平台优化登录页面和首页;
项目看板增加多个统计数据和详细数据信息,看板布局和样式优化
1 files added
27 files modified
3768 ■■■■ changed files
测试组/Test_platform/Interface_automation/.env.example 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/README.md 3 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/serializers.py 9 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tasks.py 2 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/urls.py 1 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py 535 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/prepare.py 9 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/qy_message.py 16 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/project.py 10 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/apps/schema/request.py 20 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/report.py 3 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/celery/Dockerfile 2 ●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/django/Dockerfile 2 ●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/deployment/proxy/Dockerfile 2 ●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/docker-compose.yml 4 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/DebugTalk.vue 115 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue 481 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectDashBoard.vue 1136 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue 444 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/pages/reports/ReportList.vue 964 ●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/src/restful/api.js 6 ●●●●● patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/static/favicon.ico patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/frontend/static/favicon.ico.backup patch | view | raw | blame | history
测试组/脚本/Change_password/dbExcel/数据库信息.xlsx patch | view | raw | blame | history
测试组/脚本/造数脚本2/Util/__pycache__/dingtalk_helper.cpython-312.pyc patch | view | raw | blame | history
测试组/脚本/造数脚本2/Util/__pycache__/random_util.cpython-312.pyc patch | view | raw | blame | history
测试组/脚本/造数脚本2/Util/__pycache__/stress_test_report_generator.cpython-312.pyc patch | view | raw | blame | history
测试组/Test_platform/Interface_automation/.env.example
@@ -1,12 +1,12 @@
# docker部署时将.env.example文件重命名为.env
# 数据库地址
DATABASE_HOST=lunar-link-mysql
DATABASE_HOST=127.0.0.1
# 数据库端口
DATABASE_PORT=3306
# 数据库用户名
DATABASE_USER=root
# 数据库密码
DATABASE_PASSWORD="root"
DATABASE_PASSWORD="HYBdsrs20000409M"
# 数据库名
DATABASE_NAME=lunarlink
测试组/Test_platform/Interface_automation/README.md
@@ -1,5 +1,4 @@
## 平台简介
基于HttpRunner + Django + Vue + Element UI 的接口自动化测试平台,生产可用。
## 技术栈
@@ -116,4 +115,4 @@
```
##  Docker构建
请参考文档[Docker构建](deployment/README.md)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/serializers.py
@@ -16,6 +16,7 @@
from croniter import croniter
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.db.models import Q
from django_celery_beat.models import PeriodicTask
@@ -39,6 +40,13 @@
        read_only=True,
    )
    updater_name = serializers.SerializerMethodField(read_only=True)
    groups = serializers.PrimaryKeyRelatedField(
        many=True,
        read_only=False,
        queryset=Group.objects.all(),
        required=False,
        allow_null=True
    )
    class Meta:
        model = models.Project
@@ -58,6 +66,7 @@
            "api_cover_rate",
            "jira_project_key",
            "jira_bearer_token",
            "groups",
        ]
    def get_updater_name(self, obj):
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/tasks.py
@@ -366,6 +366,8 @@
                email_recipient=email_recipient,
                email_cc=email_cc,
                case_count=len(args),
                creator=creator,
                updater=updater,
            )
            dingtalk_helper.send_markdown(title=t_name, text=message_text)
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/urls.py
@@ -44,6 +44,7 @@
    ),
    path("project/<int:pk>", project.ProjectView.as_view({"get": "single"})),
    path("project/yapi/<int:pk>", project.ProjectView.as_view({"get": "yapi_info"})),
    path("project/groups", project.ProjectView.as_view({"get": "group_list"})),
    path("dashboard", project.DashBoardView.as_view()),
    # 二叉树接口
    path("tree/<int:pk>", project.TreeView.as_view()),
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/message_template.py
@@ -10,6 +10,7 @@
from typing import Dict
from django.conf import settings
import time
from datetime import datetime
def parse_message(summary: Dict, **kwargs):
@@ -25,14 +26,79 @@
    pass_count = summary["stat"]["successes"]
    fail_count = summary["stat"]["failures"]
    error_count = summary["stat"]["errors"]
    skipped_count = summary["stat"].get("skipped", 0)
    expected_failures_count = summary["stat"].get("expectedFailures", 0)
    unexpected_successes_count = summary["stat"].get("unexpectedSuccesses", 0)
    duration = "%.2fs" % summary["time"]["duration"]
    report_id = summary["report_id"]
    base_url = settings.IM_REPORT_SETTING.get("base_url")
    port = settings.IM_REPORT_SETTING.get("port")
    report_url = f"{base_url}:{port}/api/lunarlink/reports/{report_id}"
    executed = rows_count
    fail_rate = "{:.2%}".format(fail_count / executed)
    fail_rate = "{:.2%}".format(fail_count / executed) if executed > 0 else "0.00%"
    success_rate = "{:.2%}".format(pass_count / executed) if executed > 0 else "0.00%"
    case_count = kwargs.get("case_count")
    creator = kwargs.get("creator", "")
    updater = kwargs.get("updater", "")
    start_at = summary.get("time", {}).get("start_at", 0)
    if start_at:
        start_time = datetime.fromtimestamp(start_at).strftime("%Y-%m-%d %H:%M:%S")
        end_time = datetime.fromtimestamp(start_at + summary["time"]["duration"]).strftime("%Y-%m-%d %H:%M:%S")
    else:
        start_time = ""
        end_time = ""
    setup_hooks_duration = summary.get("time", {}).get("setup_hooks_duration", 0)
    teardown_hooks_duration = summary.get("time", {}).get("teardown_hooks_duration", 0)
    platform_info = summary.get("platform", {})
    python_version = platform_info.get("python_version", "")
    httprunner_version = platform_info.get("httprunner_version", "")
    avg_duration = 0
    if executed > 0:
        avg_duration = summary["time"]["duration"] / executed
    failed_cases = []
    error_cases = []
    details = summary.get("details", [])
    for detail in details:
        records = detail.get("records", [])
        for record in records:
            if record.get("status") in ["failure", "error"]:
                case_name = record.get("name", "未知用例")
                case_status = record.get("status", "")
                meta_data = record.get("meta_data", {})
                case_url = meta_data.get("request", {}).get("url", "")
                case_method = meta_data.get("request", {}).get("method", "")
                case_error = record.get("attachment", "")
                if case_error:
                    case_error_lines = case_error.split('\n')
                    if len(case_error_lines) > 1:
                        case_error = case_error_lines[0]
                        if len(case_error_lines) > 2:
                            case_error += f"\n... (共{len(case_error_lines)}行错误信息)"
                    case_error = case_error.strip()
                else:
                    case_error = "无错误信息"
                case_info = {
                    "name": case_name,
                    "url": case_url,
                    "method": case_method,
                    "error": case_error[:150] if case_error else "无错误信息"
                }
                if case_status == "failure":
                    failed_cases.append(case_info)
                elif case_status == "error":
                    error_cases.append(case_info)
    failed_cases = failed_cases[:10]
    error_cases = error_cases[:10]
    return {
        "task_name": task_name,
@@ -42,7 +108,22 @@
        "error_count": error_count,
        "fail_count": fail_count,
        "fail_rate": fail_rate,
        "success_rate": success_rate,
        "report_url": report_url,
        "creator": creator,
        "updater": updater,
        "start_time": start_time,
        "end_time": end_time,
        "skipped_count": skipped_count,
        "expected_failures_count": expected_failures_count,
        "unexpected_successes_count": unexpected_successes_count,
        "setup_hooks_duration": setup_hooks_duration,
        "teardown_hooks_duration": teardown_hooks_duration,
        "python_version": python_version,
        "httprunner_version": httprunner_version,
        "avg_duration": avg_duration,
        "failed_cases": failed_cases,
        "error_cases": error_cases,
    }
@@ -55,6 +136,21 @@
    fail_count,
    fail_rate,
    report_url,
    success_rate,
    creator,
    updater,
    start_time,
    failed_cases,
    error_cases,
    end_time,
    skipped_count,
    expected_failures_count,
    unexpected_successes_count,
    setup_hooks_duration,
    teardown_hooks_duration,
    python_version,
    httprunner_version,
    avg_duration,
):
    """
    定制邮件报告消息模板(高级优化版)
@@ -67,72 +163,172 @@
    :param fail_count: 失败接口个数
    :param fail_rate: 失败比例
    :param report_url: 报告链接
    :param success_rate: 成功率
    :param creator: 创建人
    :param updater: 更新人
    :param start_time: 开始时间
    :param failed_cases: 失败用例列表
    :param error_cases: 异常用例列表
    :param end_time: 结束时间
    :param skipped_count: 跳过用例数
    :param expected_failures_count: 预期失败数
    :param unexpected_successes_count: 意外成功数
    :param setup_hooks_duration: 前置钩子耗时
    :param teardown_hooks_duration: 后置钩子耗时
    :param python_version: Python版本
    :param httprunner_version: HttpRunner版本
    :param avg_duration: 平均用例耗时
    :return: 邮件主题及 HTML 内容
    """
    email_subject = f"【自动化测试报告】{task_name}"
    status_icon = "✅" if fail_count == 0 and error_count == 0 else ("⚠️" if fail_count == 0 else "❌")
    if error_count == 0 and fail_count == 0:
        status_text = "测试通过"
        status_color = "#2ecc71"
        status_bg = "#e8f8f5"
    elif fail_count == 0:
        status_text = "部分异常"
        status_color = "#f39c12"
        status_bg = "#fef5e7"
    else:
        status_text = "测试失败"
        status_color = "#e74c3c"
        status_bg = "#fdedec"
    failed_cases_html = ""
    if failed_cases:
        failed_cases_html = "<div class='case-list'>"
        for idx, case in enumerate(failed_cases, 1):
            failed_cases_html += f"""
            <div class='case-item'>
              <div class='case-header'>
                <span class='case-index'>{idx}</span>
                <span class='case-name'>{case['name']}</span>
              </div>
              <div class='case-details'>
                <span class='case-method'>{case['method']}</span>
                <span class='case-url'>{case['url'][:60]}...</span>
              </div>
              <div class='case-error'>❌ {case['error']}</div>
            </div>
            """
        failed_cases_html += "</div>"
    error_cases_html = ""
    if error_cases:
        error_cases_html = "<div class='case-list'>"
        for idx, case in enumerate(error_cases, 1):
            error_cases_html += f"""
            <div class='case-item'>
              <div class='case-header'>
                <span class='case-index'>{idx}</span>
                <span class='case-name'>{case['name']}</span>
              </div>
              <div class='case-details'>
                <span class='case-method'>{case['method']}</span>
                <span class='case-url'>{case['url'][:60]}...</span>
              </div>
              <div class='case-error'>⚠️ {case['error']}</div>
            </div>
            """
        error_cases_html += "</div>"
    email_content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自动化测试报告</title>
  <!-- 引入 Google 字体 -->
  <link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet">
  <style>
    body {{
      margin: 0;
      padding: 0;
      background-color: #f5f7fa;
      font-family: 'Roboto', sans-serif;
      font-family: 'Roboto', 'Microsoft YaHei', sans-serif;
      color: #333;
    }}
    .container {{
      max-width: 700px;
      max-width: 800px;
      margin: 40px auto;
      background: #fff;
      border-radius: 12px;
      border-radius: 16px;
      overflow: hidden;
      box-shadow: 0 8px 24px rgba(0,0,0,0.1);
      box-shadow: 0 10px 40px rgba(0,0,0,0.1);
    }}
    .header {{
      background: linear-gradient(135deg, #4facfe, #00f2fe);
      padding: 30px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      padding: 40px 30px;
      text-align: center;
      color: #fff;
    }}
    .header h1 {{
      margin: 0;
      font-size: 28px;
      font-size: 32px;
      font-weight: 700;
      letter-spacing: 1px;
    }}
    .header p {{
      margin: 8px 0 0;
    .header .subtitle {{
      margin: 12px 0 0;
      font-size: 16px;
      opacity: 0.95;
    }}
    .status-badge {{
      display: inline-block;
      margin-top: 20px;
      padding: 8px 24px;
      background: rgba(255,255,255,0.2);
      border-radius: 20px;
      font-size: 14px;
      font-weight: 500;
    }}
    .content {{
      padding: 30px 40px;
      padding: 40px 50px;
    }}
    .section-title {{
      font-size: 18px;
      font-weight: 600;
      color: #2c3e50;
      margin-bottom: 20px;
      padding-bottom: 10px;
      border-bottom: 2px solid #f0f0f0;
    }}
    .summary-grid {{
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 20px;
      margin-bottom: 30px;
    }}
    .summary-grid-extended {{
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 20px;
      margin-top: 20px;
      margin-bottom: 30px;
    }}
    .card {{
      background: #f9fafc;
      border-radius: 8px;
      padding: 20px;
      background: #f8f9fa;
      border-radius: 12px;
      padding: 24px 16px;
      text-align: center;
      box-shadow: 0 2px 6px rgba(0,0,0,0.05);
      box-shadow: 0 2px 8px rgba(0,0,0,0.05);
      transition: transform 0.2s ease, box-shadow 0.2s ease;
    }}
    .card:hover {{
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }}
    .card .label {{
      font-size: 14px;
      color: #777;
      margin-bottom: 8px;
      font-size: 13px;
      color: #7f8c8d;
      margin-bottom: 10px;
      display: block;
      font-weight: 500;
    }}
    .card .value {{
      font-size: 24px;
      font-size: 28px;
      font-weight: 700;
      line-height: 1;
    }}
    .value.success {{
      color: #2ecc71;
@@ -143,40 +339,178 @@
    .value.warning {{
      color: #f39c12;
    }}
    .value.primary {{
      color: #667eea;
    }}
    .info-table {{
      width: 100%;
      border-collapse: collapse;
      margin-bottom: 30px;
    }}
    .info-table td {{
      padding: 12px 0;
      border-bottom: 1px solid #f0f0f0;
    }}
    .info-table td:first-child {{
      color: #7f8c8d;
      font-weight: 500;
      width: 120px;
    }}
    .info-table td:last-child {{
      color: #2c3e50;
      font-weight: 600;
    }}
    .progress-bar {{
      width: 100%;
      height: 8px;
      background: #e0e0e0;
      border-radius: 4px;
      overflow: hidden;
      margin: 20px 0;
    }}
    .progress-fill {{
      height: 100%;
      background: linear-gradient(90deg, #2ecc71, #27ae60);
      border-radius: 4px;
      transition: width 0.3s ease;
    }}
    .case-list {{
      margin-top: 15px;
    }}
    .case-item {{
      background: #fff;
      border: 1px solid #e9ecef;
      border-radius: 8px;
      padding: 16px;
      margin-bottom: 12px;
      transition: box-shadow 0.2s ease;
    }}
    .case-item:hover {{
      box-shadow: 0 2px 8px rgba(0,0,0,0.08);
    }}
    .case-header {{
      display: flex;
      align-items: center;
      margin-bottom: 10px;
    }}
    .case-index {{
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 24px;
      height: 24px;
      background: #667eea;
      color: #fff;
      border-radius: 50%;
      font-size: 12px;
      font-weight: 600;
      margin-right: 10px;
    }}
    .case-name {{
      font-weight: 600;
      color: #2c3e50;
      font-size: 14px;
      flex: 1;
    }}
    .case-details {{
      display: flex;
      align-items: center;
      gap: 10px;
      margin-bottom: 8px;
    }}
    .case-method {{
      display: inline-block;
      padding: 4px 10px;
      background: #e8f4f8;
      color: #2980b9;
      border-radius: 4px;
      font-size: 12px;
      font-weight: 600;
    }}
    .case-url {{
      flex: 1;
      color: #7f8c8d;
      font-size: 12px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }}
    .case-error {{
      background: #fdedec;
      color: #c0392b;
      padding: 10px 12px;
      border-radius: 6px;
      font-size: 12px;
      font-family: 'Courier New', monospace;
      white-space: pre-wrap;
      word-break: break-word;
      line-height: 1.5;
    }}
    .no-cases {{
      text-align: center;
      padding: 30px;
      color: #95a5a6;
      font-size: 14px;
    }}
    .report-btn {{
      display: block;
      width: 240px;
      width: 280px;
      margin: 30px auto 0;
      text-align: center;
      padding: 15px;
      background-color: #4a90e2;
      padding: 16px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: #fff;
      text-decoration: none;
      border-radius: 6px;
      font-weight: 500;
      transition: background 0.3s ease;
      border-radius: 8px;
      font-weight: 600;
      font-size: 16px;
      transition: transform 0.2s ease, box-shadow 0.2s ease;
      box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
    }}
    .report-btn:hover {{
      background-color: #3b7dd8;
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
    }}
    .footer {{
      text-align: center;
      font-size: 12px;
      color: #aaa;
      padding: 20px;
      background: #f0f2f5;
      font-size: 13px;
      color: #95a5a6;
      padding: 25px;
      background: #f8f9fa;
      border-top: 1px solid #e9ecef;
    }}
    .divider {{
      height: 1px;
      background: #e9ecef;
      margin: 30px 0;
    }}
    .icon {{
      font-size: 20px;
      margin-right: 8px;
    }}
    @media (max-width: 768px) {{
      .container {{
        margin: 20px;
      }}
      .content {{
        padding: 30px 25px;
      }}
      .summary-grid {{
        grid-template-columns: 1fr 1fr;
        grid-template-columns: repeat(2, 1fr);
      }}
      .summary-grid-extended {{
        grid-template-columns: repeat(2, 1fr);
      }}
    }}
    @media (max-width: 480px) {{
      .summary-grid {{
        grid-template-columns: 1fr;
      }}
      .summary-grid-extended {{
        grid-template-columns: 1fr;
      }}
      .header h1 {{
        font-size: 24px;
      }}
    }}
  </style>
@@ -184,40 +518,147 @@
<body>
  <div class="container">
    <div class="header">
      <h1>自动化测试报告</h1>
      <p>{task_name}</p>
      <h1>🚀 自动化测试报告</h1>
      <p class="subtitle">{task_name}</p>
      <div class="status-badge">{status_icon} {status_text}</div>
    </div>
    <div class="content">
      <div class="section-title">📊 测试概览</div>
      <div class="summary-grid">
        <div class="card">
          <span class="label">总耗时</span>
          <span class="value">{duration}</span>
          <span class="value primary">{duration}</span>
        </div>
        <div class="card">
          <span class="label">用例个数</span>
          <span class="label">用例总数</span>
          <span class="value">{case_count}</span>
        </div>
        <div class="card">
          <span class="label">失败比例</span>
          <span class="value warning">{fail_rate}</span>
          <span class="label">成功率</span>
          <span class="value success">{success_rate}</span>
        </div>
        <div class="card">
          <span class="label">成功接口</span>
          <span class="label">失败率</span>
          <span class="value error">{fail_rate}</span>
        </div>
        <div class="card">
          <span class="label">✅ 成功</span>
          <span class="value success">{pass_count}</span>
        </div>
        <div class="card">
          <span class="label">异常接口</span>
          <span class="value error">{error_count}</span>
        </div>
        <div class="card">
          <span class="label">失败接口</span>
          <span class="label">❌ 失败</span>
          <span class="value error">{fail_count}</span>
        </div>
        <div class="card">
          <span class="label">⚠️ 异常</span>
          <span class="value warning">{error_count}</span>
      </div>
      <a href="{report_url}" class="report-btn">点击查看详细报告</a>
        <div class="card">
          <span class="label">📈 通过率</span>
          <span class="value success">{success_rate}</span>
    </div>
      </div>
      <div class="summary-grid-extended">
        <div class="card">
          <span class="label">⏭️ 跳过</span>
          <span class="value" style="color: #95a5a6;">{skipped_count}</span>
        </div>
        <div class="card">
          <span class="label">🎯 预期失败</span>
          <span class="value" style="color: #f39c12;">{expected_failures_count}</span>
        </div>
        <div class="card">
          <span class="label">🎉 意外成功</span>
          <span class="value" style="color: #3498db;">{unexpected_successes_count}</span>
        </div>
      </div>
      <div class="summary-grid-extended">
        <div class="card">
          <span class="label">⚡ 平均耗时</span>
          <span class="value primary">{avg_duration:.3f}s</span>
        </div>
        <div class="card">
          <span class="label">🔧 前置钩子</span>
          <span class="value" style="color: #7f8c8d;">{setup_hooks_duration:.3f}s</span>
        </div>
        <div class="card">
          <span class="label">🔧 后置钩子</span>
          <span class="value" style="color: #7f8c8d;">{teardown_hooks_duration:.3f}s</span>
        </div>
      </div>
      <div class="divider"></div>
      <div class="section-title">📋 详细信息</div>
      <table class="info-table">
        <tr>
          <td><span class="icon">📝</span>任务名称</td>
          <td>{task_name}</td>
        </tr>
        <tr>
          <td><span class="icon">⏱️</span>执行耗时</td>
          <td>{duration}</td>
        </tr>
        <tr>
          <td><span class="icon">⚡</span>平均耗时</td>
          <td>{avg_duration:.3f}s</td>
        </tr>
        <tr>
          <td><span class="icon">📅</span>开始时间</td>
          <td>{start_time if start_time else '未记录'}</td>
        </tr>
        <tr>
          <td><span class="icon">🏁</span>结束时间</td>
          <td>{end_time if end_time else '未记录'}</td>
        </tr>
        <tr>
          <td><span class="icon">👤</span>创建人</td>
          <td>{creator if creator else '未记录'}</td>
        </tr>
        <tr>
          <td><span class="icon">✏️</span>更新人</td>
          <td>{updater if updater else '未记录'}</td>
        </tr>
        <tr>
          <td><span class="icon">🐍</span>Python版本</td>
          <td>{python_version if python_version else '未记录'}</td>
        </tr>
        <tr>
          <td><span class="icon">🏃</span>HttpRunner版本</td>
          <td>{httprunner_version if httprunner_version else '未记录'}</td>
        </tr>
      </table>
      <div class="divider"></div>
      <div class="section-title">📈 测试结果分析</div>
      <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
        <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
          <span style="color: #7f8c8d; font-size: 14px;">测试通过率</span>
          <span style="color: #2ecc71; font-weight: 700; font-size: 18px;">{success_rate}</span>
        </div>
        <div class="progress-bar">
          <div class="progress-fill" style="width: {success_rate};"></div>
        </div>
        <div style="display: flex; justify-content: space-between; font-size: 12px; color: #95a5a6;">
          <span>成功: {pass_count}</span>
          <span>失败: {fail_count}</span>
          <span>异常: {error_count}</span>
        </div>
      </div>
      {f'<div class="divider"></div><div class="section-title">❌ 失败用例列表</div>{failed_cases_html}' if failed_cases else ''}
      {f'<div class="divider"></div><div class="section-title">⚠️ 异常用例列表</div>{error_cases_html}' if error_cases else ''}
      <a href="{report_url}" class="report-btn">👉 点击查看详细报告</a>
    </div>
    <div class="footer">
      本邮件由系统自动发出,请勿回复
      <p>本邮件由系统自动发出,请勿回复</p>
      <p style="margin-top: 8px;">如有疑问,请联系相关负责人</p>
    </div>
  </div>
</body>
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/prepare.py
@@ -136,12 +136,17 @@
def aggregate_reports_by_status(project_id) -> Tuple[List, List]:
    """按照状态统计项目中的报告"""
    from datetime import datetime, timedelta
    query = models.Report.objects
    if project_id:
        query = query.filter(project_id=project_id)
    # 只统计最近7天的数据
    seven_days_ago = datetime.now() - timedelta(days=7)
    query = query.filter(create_time__gte=seven_days_ago)
    report_count: Dict = query.aggregate(
        失败=Count("pk", filter=Q(status=0)),
        成功=Count("pk", filter=Q(status=1)),
        失败=Count("pk", filter=Q(status=False)),
        成功=Count("pk", filter=Q(status=True)),
    )
    return list(report_count.keys()), list(report_count.values())
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/utils/qy_message.py
@@ -51,11 +51,16 @@
        mentioned_list = []
    webhook = settings.QY_WEB_HOOK
    # 检查webhook是否为空
    if not webhook:
        logger.warning("企业微信webhook为空,跳过发送通知")
        return
    header = {"Content-Type": "application/json"}
    content = f"""<font color=\'info\'>**LunarLink平台预警**</font> \n
    >url: <font color=\'comment\'>{msg.get("url")}</font>
    >msg: <font color=\'comment\'>{msg.get("msg")}</font>
    >traceback: <font color=\'warning\'>{msg.get("traceback")}</font>"""
    content = f"""<font color='info'>**LunarLink平台预警**</font> \n
    >url: <font color='comment'>{msg.get("url")}</font>
    >msg: <font color='comment'>{msg.get("msg")}</font>
    >traceback: <font color='warning'>{msg.get("traceback")}</font>"""
    data = {
        "msgtype": "markdown",
        "markdown": {
@@ -64,8 +69,11 @@
            "mentioned_mobile_list": mentioned_mobile_list,
        },
    }
    try:
    res = requests.post(url=webhook, headers=header, json=data).json()
    if res.get("errcode") == 0:
        logger.info(f"发送通知成功,请求的webhook是: {webhook}")
    else:
        logger.error(f"发送通知失败,请求的webhook是: {webhook}, 响应是:{res}")
    except Exception as e:
        logger.error(f"发送通知异常,请求的webhook是: {webhook}, 异常信息:{str(e)}")
测试组/Test_platform/Interface_automation/backend/apps/lunarlink/views/project.py
@@ -9,6 +9,7 @@
"""
from typing import Dict
from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError, transaction
from django.db.models import Count
@@ -167,6 +168,13 @@
        ser = self.get_serializer(obj, many=False)
        return Response(ser.data)
    @method_decorator(request_log(level="INFO"))
    def group_list(self, request):
        """获取所有分组列表"""
        groups = Group.objects.all()
        group_list = [{"id": group.id, "name": group.name} for group in groups]
        return Response(group_list)
class DashBoardView(APIView):
    """项目看板"""
@@ -281,7 +289,7 @@
            # 包含今天的前6天
            "recent_days": [get_day(n)[5:] for n in range(-5, 1)],
            "recent_months": [get_month_format(n) for n in range(-5, 1)],
            "recent_weeks": [get_week_format(n) for n in range(-5, 1)],
            "recent_weeks": [get_week_format(n) for n in range(-5, 1)]
        }
        return Response(res)
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py
测试组/Test_platform/Interface_automation/backend/apps/schema/request.py
@@ -16,6 +16,22 @@
body = TypeVar("body", bytes, str)
def convert_cookies_to_dict(cookies):
    """
    将 mitmproxy 的 cookies 对象转换为可 JSON 序列化的字典
    :param cookies: mitmproxy cookies 对象
    :return: 可序列化的字典
    """
    result = {}
    for key, value in cookies.items():
        if hasattr(value, 'items'):
            result[key] = dict(value.items())
        else:
            result[key] = value
    return result
class RequestInfo(BaseModel):
    url: str
    body: str
@@ -38,8 +54,8 @@
                    response_headers=dict(flow.response.headers),
                    response_content=self.get_response(flow.response),
                    body=self.get_body(flow.request),
                    cookies=dict(flow.response.cookies),
                    request_cookies=dict(flow.request.cookies),
                    cookies=convert_cookies_to_dict(flow.response.cookies),
                    request_cookies=convert_cookies_to_dict(flow.request.cookies),
                )
            )
        super().__init__(**kwargs)
测试组/Test_platform/Interface_automation/backend/extra_apps/httprunner/report.py
@@ -7,6 +7,9 @@
import time
import unittest
from base64 import b64encode
try:
    from collections.abc import Iterable
except ImportError:
from collections import Iterable
from datetime import datetime
测试组/Test_platform/Interface_automation/deployment/celery/Dockerfile
@@ -3,7 +3,7 @@
COPY ./backend/requirements.txt requirements.txt
# 国内打包请替换成 https://mirrors.aliyun.com/pypi/simple
ARG PIP_INDEX_URL="https://mirrors.aliyun.com/pypi/simple"
ARG PIP_INDEX_URL="https://pypi.org/simple"
# 安装依赖,尽量减少镜像层并在安装后清理
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
    sed -i 's|security.debian.org/debian-security|mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list && \
测试组/Test_platform/Interface_automation/deployment/django/Dockerfile
@@ -3,7 +3,7 @@
COPY ./backend/requirements.txt requirements.txt
# 国内打包请替换成 https://mirrors.aliyun.com/pypi/simple
ARG PIP_INDEX_URL="https://mirrors.aliyun.com/pypi/simple"
ARG PIP_INDEX_URL="https://pypi.org/simple"
# 安装依赖,尽量减少镜像层并在安装后清理
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
    sed -i 's|security.debian.org/debian-security|mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list && \
测试组/Test_platform/Interface_automation/deployment/proxy/Dockerfile
@@ -3,7 +3,7 @@
COPY ./backend/requirements.txt requirements.txt
# 国内打包请替换成 https://mirrors.aliyun.com/pypi/simple
ARG PIP_INDEX_URL="https://mirrors.aliyun.com/pypi/simple"
ARG PIP_INDEX_URL="https://pypi.org/simple"
# 安装依赖,尽量减少镜像层并在安装后清理
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
    sed -i 's|security.debian.org/debian-security|mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list && \
测试组/Test_platform/Interface_automation/docker-compose.yml
@@ -90,9 +90,9 @@
    privileged: true
    restart: always
    ports:
      - "3306:3306"
      - "3307:3307"
    expose:
      - 3306
      - 3307
    environment:
      MYSQL_DATABASE: "lunarlink"
      MYSQL_ROOT_PASSWORD: "root"
测试组/Test_platform/Interface_automation/frontend/src/pages/httprunner/DebugTalk.vue
@@ -18,6 +18,14 @@
                    @click="handleRunCode"
                    >在线运行</el-button
                >
                <el-button
                    round
                    icon="el-icon-document-copy"
                    type="success"
                    size="small"
                    @click="handleImportCode"
                    >插入其他项目代码</el-button
                >
            </div>
        </el-header>
@@ -44,6 +52,65 @@
        >
            <RunCodeResult :msg="resp.msg"></RunCodeResult>
        </el-drawer>
        <el-dialog
            title="插入其他项目代码"
            :visible.sync="importDialogVisible"
            width="70%"
            :close-on-click-modal="false"
        >
            <div style="margin-bottom: 20px;">
                <el-select
                    v-model="selectedProjectId"
                    placeholder="请选择项目"
                    filterable
                    @change="handleProjectChange"
                    style="width: 100%;"
                >
                    <el-option
                        v-for="project in projectList"
                        :key="project.id"
                        :label="project.name"
                        :value="project.id"
                    >
                        <span style="float: left">{{ project.name }}</span>
                        <span style="float: right; color: #8492a6; font-size: 13px">{{ project.responsible }}</span>
                    </el-option>
                </el-select>
            </div>
            <div v-if="selectedProjectCode" style="margin-bottom: 20px;">
                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
                    <span style="font-weight: bold;">代码预览:</span>
                    <el-button
                        type="primary"
                        size="small"
                        icon="el-icon-document-copy"
                        @click="handleCopyAll"
                    >
                        全部复制
                    </el-button>
                </div>
                <el-input
                    type="textarea"
                    :rows="15"
                    v-model="selectedProjectCode"
                    readonly
                    style="font-family: 'Courier New', monospace;"
                ></el-input>
            </div>
            <span slot="footer" class="dialog-footer">
                <el-button @click="importDialogVisible = false">取 消</el-button>
                <el-button
                    type="primary"
                    @click="handleConfirmImport"
                    :disabled="!selectedProjectCode"
                >
                    确定插入
                </el-button>
            </span>
        </el-dialog>
    </el-container>
</template>
@@ -64,6 +131,10 @@
            editor: null,
            timeStamp: "",
            isShowDebug: false,
            importDialogVisible: false,
            projectList: [],
            selectedProjectId: "",
            selectedProjectCode: "",
            options: {
                selectOnLineNumbers: false,
                scrollbar: {
@@ -103,6 +174,50 @@
            this.$api.getDebugtalk(this.$route.params.id).then(resp => {
                this.code = resp;
            });
        },
        handleImportCode() {
            this.importDialogVisible = true;
            this.getProjectList();
        },
        getProjectList() {
            this.$api.getProjectList().then(resp => {
                this.projectList = resp.results || [];
            });
        },
        handleProjectChange(projectId) {
            if (projectId) {
                this.$api.getDebugtalk(projectId).then(resp => {
                    this.selectedProjectCode = resp.code || "";
                }).catch(err => {
                    this.$message.error("获取项目驱动代码失败");
                    this.selectedProjectCode = "";
                });
            }
        },
        handleCopyAll() {
            if (this.selectedProjectCode) {
                const textarea = document.createElement("textarea");
                textarea.value = this.selectedProjectCode;
                document.body.appendChild(textarea);
                textarea.select();
                try {
                    document.execCommand("copy");
                    this.$message.success("代码已复制到剪贴板");
                } catch (err) {
                    this.$message.error("复制失败,请手动复制");
                }
                document.body.removeChild(textarea);
            }
        },
        handleConfirmImport() {
            if (this.selectedProjectCode && this.editor) {
                const currentCode = this.editor.getValue();
                const newCode = currentCode + "\n\n" + this.selectedProjectCode;
                this.editor.setValue(newCode);
                this.code.code = newCode;
                this.$message.success("代码已插入");
                this.importDialogVisible = false;
            }
        }
    },
    watch: {
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue
@@ -1,17 +1,79 @@
<template>
  <div class="login-container">
<!--    <div class="login-background">-->
<!--      <div class="gradient-overlay"></div>-->
<!--    </div>-->
    <!-- 动态背景 -->
    <div class="background-animation">
      <div class="floating-shapes">
        <div class="shape shape-1"></div>
        <div class="shape shape-2"></div>
        <div class="shape shape-3"></div>
        <div class="shape shape-4"></div>
      </div>
      <div class="network-lines"></div>
      <div class="gradient-overlay"></div>
    </div>
    <el-card class="login-card">
    <!-- 主登录区域 -->
    <div class="login-content">
      <!-- 左侧品牌展示区 -->
      <div class="brand-section">
        <img
          src="~@/assets/images/img.png"
          class="brand-logo"
          alt="系统logo"
        />
        <h1 class="system-name">接口自动化平台</h1>
        <div class="brand-logo-container">
          <div class="logo-icon">
            <svg viewBox="0 0 100 100" class="api-icon">
              <!-- 主API连接图标 -->
              <path d="M20,30 L50,10 L80,30 L80,70 L50,90 L20,70 Z" fill="rgba(0,212,255,0.1)" stroke="rgba(0,212,255,0.6)" stroke-width="2"/>
              <!-- 内部连接层 -->
              <path d="M30,40 L50,25 L70,40 L70,60 L50,75 L30,60 Z" fill="rgba(148,0,211,0.1)" stroke="rgba(148,0,211,0.6)" stroke-width="1.5"/>
              <!-- 核心数据点 -->
              <path d="M40,50 L50,40 L60,50 L60,55 L50,60 L40,55 Z" fill="rgba(255,255,255,0.2)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
              <!-- 动态连接线 -->
              <line x1="35" y1="45" x2="45" y2="52" stroke="#00d4ff" stroke-width="1.5" stroke-dasharray="2,2"/>
              <line x1="55" y1="45" x2="65" y2="52" stroke="#9400d3" stroke-width="1.5" stroke-dasharray="2,2"/>
              <!-- 中心数据流点 -->
              <circle cx="50" cy="50" r="4" fill="#00d4ff">
                <animate attributeName="r" values="4;6;4" dur="2s" repeatCount="indefinite"/>
                <animate attributeName="opacity" values="1;0.7;1" dur="2s" repeatCount="indefinite"/>
              </circle>
              <!-- 外围数据点 -->
              <circle cx="35" cy="35" r="2" fill="#00d4ff">
                <animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite"/>
              </circle>
              <circle cx="65" cy="35" r="2" fill="#9400d3">
                <animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite" begin="0.5s"/>
              </circle>
              <circle cx="35" cy="65" r="2" fill="#9400d3">
                <animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite" begin="1s"/>
              </circle>
              <circle cx="65" cy="65" r="2" fill="#00d4ff">
                <animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite" begin="1.5s"/>
              </circle>
            </svg>
          </div>
          <h1 class="system-name">APITest Pro</h1>
          <p class="system-desc">智能接口自动化测试平台</p>
        </div>
        <div class="feature-list">
          <div class="feature-item">
            <span class="feature-icon">🚀</span>
            <span>高效自动化测试</span>
          </div>
          <div class="feature-item">
            <span class="feature-icon">🔗</span>
            <span>智能接口管理</span>
          </div>
          <div class="feature-item">
            <span class="feature-icon">📊</span>
            <span>实时数据监控</span>
          </div>
        </div>
      </div>
      <!-- 右侧登录表单 -->
      <div class="login-form-section">
        <div class="form-container">
          <div class="form-header">
            <h2>欢迎登录</h2>
            <p>请使用您的账号密码登录系统</p>
      </div>
      <el-form
@@ -19,35 +81,37 @@
        @submit.native.prevent="submitForm"
        class="login-form"
      >
        <el-form-item>
            <div class="input-group">
              <label class="input-label">用户名</label>
          <el-input
            v-model="loginForm.username"
            placeholder="账号"
            prefix-icon="el-icon-user-solid"
            class="custom-input"
                placeholder="请输入用户名"
                prefix-icon="el-icon-user"
                class="modern-input"
            :class="{ 'input-error': usernameInvalid }"
            @blur="validateUserName"
          />
          <transition name="el-zoom-in-top">
              <transition name="slide-fade">
            <div v-if="usernameInvalid" class="error-msg">{{ usernameInvalid }}</div>
          </transition>
        </el-form-item>
            </div>
        <el-form-item>
            <div class="input-group">
              <label class="input-label">密码</label>
          <el-input
            v-model="loginForm.password"
            type="password"
            placeholder="密码"
                placeholder="请输入密码"
            prefix-icon="el-icon-lock"
            show-password
            class="custom-input"
                class="modern-input"
            :class="{ 'input-error': passwordInvalid }"
            @blur="validatePassword"
          />
          <transition name="el-zoom-in-top">
              <transition name="slide-fade">
            <div v-if="passwordInvalid" class="error-msg">{{ passwordInvalid }}</div>
          </transition>
        </el-form-item>
            </div>
        <el-button
          type="primary"
@@ -55,10 +119,17 @@
          :loading="isLoading"
          @click="submitForm"
        >
          {{ isLoading ? '登录中...' : '立即登录' }}
              {{ isLoading ? '登录中...' : '登录系统' }}
        </el-button>
      </el-form>
    </el-card>
          <div class="form-footer">
            <p>还没有账号?<a href="#" class="register-link">联系管理员注册</a></p>
            <p class="copyright">© 2025 APITest Pro 智能接口自动化平台</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
@@ -151,56 +222,374 @@
<style scoped>
.login-container {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  background: linear-gradient(135deg, #0c0e27 0%, #1a1f3d 50%, #2d1b69 100%);
  position: relative;
  overflow: hidden;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.login-background {
/* 动态背景动画 */
.background-animation {
  position: absolute;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.1);
  opacity: 0.1;
  top: 0;
  left: 0;
}
.floating-shapes {
  position: absolute;
  width: 100%;
  height: 100%;
}
.shape {
  position: absolute;
  border-radius: 50%;
  background: linear-gradient(45deg, rgba(0, 212, 255, 0.1), rgba(148, 0, 211, 0.1));
  animation: float 20s infinite linear;
}
.shape-1 {
  width: 100px;
  height: 100px;
  top: 10%;
  left: 10%;
  animation-delay: 0s;
}
.shape-2 {
  width: 150px;
  height: 150px;
  top: 60%;
  right: 10%;
  animation-delay: -5s;
}
.shape-3 {
  width: 80px;
  height: 80px;
  bottom: 20%;
  left: 20%;
  animation-delay: -10s;
}
.shape-4 {
  width: 120px;
  height: 120px;
  top: 30%;
  right: 30%;
  animation-delay: -15s;
}
.network-lines {
  position: absolute;
  width: 100%;
  height: 100%;
  background-image:
    linear-gradient(90deg, transparent 24px, rgba(0, 212, 255, 0.03) 25px, rgba(0, 212, 255, 0.03) 26px, transparent 27px, transparent 74px, rgba(148, 0, 211, 0.03) 75px, rgba(148, 0, 211, 0.03) 76px, transparent 77px),
    linear-gradient(0deg, transparent 24px, rgba(0, 212, 255, 0.03) 25px, rgba(0, 212, 255, 0.03) 26px, transparent 27px, transparent 74px, rgba(148, 0, 211, 0.03) 75px, rgba(148, 0, 211, 0.03) 76px, transparent 77px);
  background-size: 100px 100px;
  animation: gridMove 40s linear infinite;
}
.gradient-overlay {
  position: absolute;
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
  background: radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
              radial-gradient(circle at 80% 20%, rgba(148, 0, 211, 0.1) 0%, transparent 50%);
}
.login-card {
  width: 420px;
  border-radius: 12px;
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
  border: none;
  z-index: 1;
/* 主内容区域 */
.login-content {
  display: flex;
  min-height: 100vh;
  max-width: 1400px;
  margin: 0 auto;
  position: relative;
  z-index: 2;
}
/* 左侧品牌区域 */
.brand-section {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 4rem;
  color: white;
}
.brand-logo-container {
  margin-bottom: 3rem;
}
.logo-icon {
  width: 120px;
  height: 120px;
  margin-bottom: 1.5rem;
  filter: drop-shadow(0 0 20px rgba(0, 212, 255, 0.5));
}
.api-icon {
  width: 100%;
  height: 100%;
  animation: pulse 3s ease-in-out infinite;
}
.system-name {
  font-size: 3rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
  background: linear-gradient(135deg, #00d4ff 0%, #9400d3 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.system-desc {
  font-size: 1.2rem;
  opacity: 0.8;
  margin: 0;
}
.feature-list {
  margin-top: 2rem;
}
.feature-item {
  display: flex;
  align-items: center;
  margin-bottom: 1rem;
  font-size: 1.1rem;
  opacity: 0.9;
  transition: opacity 0.3s ease;
}
.feature-item:hover {
  opacity: 1;
}
.feature-icon {
  font-size: 1.5rem;
  margin-right: 0.8rem;
  filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.5));
}
/* 右侧表单区域 */
.login-form-section {
  flex: 0 0 500px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
.form-container {
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  border-radius: 20px;
  padding: 3rem 2.5rem;
  width: 100%;
  max-width: 400px;
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
}
.form-header {
  text-align: center;
  margin-bottom: 2.5rem;
}
.form-header h2 {
  font-size: 2rem;
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: #1a1f3d;
}
.form-header p {
  color: #666;
  margin: 0;
}
.input-group {
  margin-bottom: 1.5rem;
}
.input-label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: #333;
  font-size: 0.9rem;
}
.modern-input {
  width: 100%;
}
.modern-input >>> .el-input__inner {
  height: 48px;
  border-radius: 12px;
  border: 2px solid #e1e5e9;
  font-size: 1rem;
  transition: all 0.3s ease;
  background: #f8f9fa;
}
.modern-input >>> .el-input__inner:focus {
  border-color: #00d4ff;
  box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
  background: white;
}
.modern-input.input-error >>> .el-input__inner {
  border-color: #ff4757;
}
.login-button {
  width: 100%;
  height: 50px;
  border-radius: 12px;
  font-size: 1.1rem;
  font-weight: 600;
  background: linear-gradient(135deg, #00d4ff 0%, #9400d3 100%);
  border: none;
  transition: all 0.3s ease;
  margin-top: 0.5rem;
}
.login-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 25px rgba(0, 212, 255, 0.4);
}
.login-button:active {
  transform: translateY(0);
}
.error-msg {
  color: #ff4757;
  font-size: 0.85rem;
  margin-top: 0.3rem;
  animation: slideIn 0.3s ease;
}
.form-footer {
  margin-top: 2rem;
  text-align: center;
}
.form-footer p {
  margin: 0.5rem 0;
  color: #666;
  font-size: 0.9rem;
}
.register-link {
  color: #00d4ff;
  text-decoration: none;
  font-weight: 500;
}
.register-link:hover {
  text-decoration: underline;
}
.copyright {
  opacity: 0.6;
  font-size: 0.8rem;
}
/* 动画效果 */
@keyframes float {
  0%, 100% {
    transform: translateY(0px) rotate(0deg);
  }
  33% {
    transform: translateY(-20px) rotate(120deg);
  }
  66% {
    transform: translateY(10px) rotate(240deg);
  }
}
@keyframes gridMove {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(100px, 100px);
  }
}
@keyframes pulse {
  0%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.05);
    opacity: 0.8;
  }
}
@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.slide-fade-enter-active {
  transition: all 0.3s ease;
}
.slide-fade-leave-active {
  transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter, .slide-fade-leave-to {
  transform: translateY(-10px);
  opacity: 0;
}
/* 响应式设计 */
@media (max-width: 1024px) {
  .login-content {
    flex-direction: column;
}
.brand-section {
    padding: 2rem;
  text-align: center;
  margin-bottom: 2rem;
}
.brand-logo {
  width: 120px;  /* 尺寸放大50% */
  height: auto;
  margin-bottom: 1.5rem;  /* 增加下边距保持间距 */
  transition: transform 0.3s ease; /* 添加悬停动效 */
  .login-form-section {
    flex: none;
    width: 100%;
  }
}
/* 可选悬停效果 */
.brand-logo:hover {
  transform: scale(1.05);
@media (max-width: 768px) {
  .form-container {
    margin: 1rem;
    padding: 2rem 1.5rem;
}
.system-name {
    font-size: 2.5rem;
  }
}
.system-name {
  font-size: 2rem;  /* 同步放大系统名称字号 */
  margin-top: 0.5rem;  /* 增加与LOGO的间距 */
}
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectDashBoard.vue
@@ -1,93 +1,329 @@
<template>
    <div style="display: flex; justify-content: space-around; flex-wrap: wrap">
        <div class="api-case">
            <el-card>
                <div slot="header">
                    <span>每日趋势</span>
                    <i class="iconfont">&#xe66e;</i>
    <div class="dashboard-container">
        <!-- 页面标题 -->
        <div class="page-header">
            <h2>项目概览</h2>
            <p class="subtitle">实时监控项目运行状态与趋势</p>
                </div>
                <el-skeleton v-if="isLoading" :rows="9" />
        <!-- 概览卡片 -->
        <div class="overview-section">
            <el-row :gutter="24">
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card success" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>成功报告</h3>
                            <i class="el-icon-check-circle"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalSuccessReports }}</div>
                            <div class="card-desc">最近7天</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 12%
                            </span>
                            <span class="compared">较上周</span>
                        </div>
                    </div>
                </el-col>
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card danger" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>失败报告</h3>
                            <i class="el-icon-close-circle"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalFailedReports }}</div>
                            <div class="card-desc">最近7天</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-down">
                                <i class="el-icon-caret-bottom"></i> 8%
                            </span>
                            <span class="compared">较上周</span>
                        </div>
                    </div>
                </el-col>
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card info" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>新增API</h3>
                            <i class="el-icon-document-add"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalNewApis }}</div>
                            <div class="card-desc">最近30天</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 23%
                            </span>
                            <span class="compared">较上月</span>
                        </div>
                    </div>
                </el-col>
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card warning" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>新增用例</h3>
                            <i class="el-icon-edit-outline"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalNewCases }}</div>
                            <div class="card-desc">最近30天</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 18%
                            </span>
                            <span class="compared">较上月</span>
                        </div>
                    </div>
                </el-col>
            </el-row>
            <!-- 第二行概览卡片 -->
            <el-row :gutter="24" style="margin-top: 24px;">
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card primary" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>API总数</h3>
                            <i class="el-icon-collection-tag"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalApis }}</div>
                            <div class="card-desc">项目总数</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 5%
                            </span>
                            <span class="compared">较上月</span>
                        </div>
                    </div>
                </el-col>
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card purple" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>用例总数</h3>
                            <i class="el-icon-document-copy"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalCases }}</div>
                            <div class="card-desc">项目总数</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 9%
                            </span>
                            <span class="compared">较上月</span>
                        </div>
                    </div>
                </el-col>
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card teal" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>报告通过率</h3>
                            <i class="el-icon-data-line"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ successRate }}%</div>
                            <div class="card-desc">最近7天</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 3%
                            </span>
                            <span class="compared">较上周</span>
                        </div>
                    </div>
                </el-col>
                <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
                    <div class="overview-card orange" @mouseenter="cardHover=true" @mouseleave="cardHover=false">
                        <div class="card-header">
                            <h3>配置总数</h3>
                            <i class="el-icon-setting"></i>
                        </div>
                        <div class="card-content">
                            <div class="card-value">{{ totalConfigs }}</div>
                            <div class="card-desc">项目总数</div>
                        </div>
                        <div class="card-footer">
                            <span class="trend-up">
                                <i class="el-icon-caret-top"></i> 12%
                            </span>
                            <span class="compared">较上月</span>
                        </div>
                    </div>
                </el-col>
            </el-row>
        </div>
        <!-- 图表区域 -->
        <div class="charts-section">
            <!-- 第一行 -->
            <el-row :gutter="24">
                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">每日趋势</span>
                            <span class="subtitle">API、用例创建趋势</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                <ApexCharts
                    v-else
                    :options="apiOptionsLine"
                    :series="apiCaseLineSeries"
                            height="350"
                ></ApexCharts>
            </el-card>
                </el-col>
                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">报告趋势</span>
                            <span class="subtitle">测试报告执行趋势</span>
        </div>
        <div class="api-case">
            <el-card>
                <div slot="header">
                    <span>每周指标</span>
                    <i class="iconfont">&#xe66e;</i>
                </div>
                <el-skeleton v-if="isLoading" :rows="9" />
                <ApexCharts
                    v-else
                    :options="optionsWeekBar"
                    :series="weekBarSeries"
                ></ApexCharts>
            </el-card>
        </div>
        <div class="api-case">
            <el-card>
                <div slot="header">
                    <span>每月指标</span>
                    <i class="iconfont">&#xe66e;</i>
                </div>
                <el-skeleton v-if="isLoading" :rows="9" />
                <ApexCharts
                    v-else
                    :options="optionsMonthBar"
                    :series="monthBarSeries"
                ></ApexCharts>
            </el-card>
        </div>
        <div class="api-case-monthly">
            <el-card>
                <div slot="header">
                    <span>近半年接口创建前5名统计</span>
                    <i class="iconfont">&#xe66e;</i>
                </div>
                <el-skeleton v-if="isLoading" :rows="9" />
                <ApexCharts
                    v-else
                    :options="apiMonthlyOptionsLine"
                    :series="apiMonthlyLineSeries"
                ></ApexCharts>
            </el-card>
        </div>
        <div class="api-case-monthly">
            <el-card>
                <div slot="header">
                    <span>近半年用例创建前5名统计</span>
                    <i class="iconfont">&#xe66e;</i>
                </div>
                <el-skeleton v-if="isLoading" :rows="9" />
                <ApexCharts
                    v-else
                    :options="caseMonthlyOptionsLine"
                    :series="caseMonthlyLineSeries"
                ></ApexCharts>
            </el-card>
        </div>
        <div class="api-case-monthly">
            <el-card>
                <div slot="header">
                    <span>报告日-周-月趋势</span>
                    <i class="iconfont">&#xe66e;</i>
                </div>
                <el-skeleton v-if="isLoading" :rows="12" />
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                <ApexCharts
                    v-else
                    :options="reportOptionsLine"
                    :series="reportLineSeries"
                            height="350"
                ></ApexCharts>
            </el-card>
                </el-col>
            </el-row>
            <!-- 第二行 -->
            <el-row :gutter="24" style="margin-top: 24px;">
                <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">每周指标</span>
                            <span class="subtitle">按周统计数据</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="8" animated />
                        <ApexCharts
                            v-else
                            :options="optionsWeekBar"
                            :series="weekBarSeries"
                            height="320"
                        ></ApexCharts>
                    </el-card>
                </el-col>
                <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">每月指标</span>
                            <span class="subtitle">按月统计数据</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="8" animated />
                        <ApexCharts
                            v-else
                            :options="optionsMonthBar"
                            :series="monthBarSeries"
                            height="320"
                        ></ApexCharts>
                    </el-card>
                </el-col>
                <el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">报告统计</span>
                            <span class="subtitle">报告类型与状态分布</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                        <div v-else class="report-stats-container">
                            <div class="report-stat-item">
                                <h4>报告类型</h4>
                                <ApexCharts
                                    :options="reportPieOptions"
                                    :series="reportPieSeries"
                                    height="200"
                                ></ApexCharts>
                            </div>
                            <div class="report-stat-item">
                                <h4>报告状态</h4>
                                <ApexCharts
                                    :options="reportRadiaOptions"
                                    :series="reportRadiaSeries"
                                    height="200"
                                ></ApexCharts>
                            </div>
                        </div>
                    </el-card>
                </el-col>
            </el-row>
            <!-- 第三行 -->
            <el-row :gutter="24" style="margin-top: 24px;">
                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">近半年接口创建前5名</span>
                            <span class="subtitle">按创建人统计</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                        <ApexCharts
                            v-else
                            :options="apiMonthlyOptionsLine"
                            :series="apiMonthlyLineSeries"
                            height="350"
                        ></ApexCharts>
                    </el-card>
                </el-col>
                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">近半年用例创建前5名</span>
                            <span class="subtitle">按创建人统计</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                        <ApexCharts
                            v-else
                            :options="caseMonthlyOptionsLine"
                            :series="caseMonthlyLineSeries"
                            height="350"
                        ></ApexCharts>
                    </el-card>
                </el-col>
            </el-row>
            <!-- 第四行 -->
            <el-row :gutter="24" style="margin-top: 24px;">
                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">每日创建趋势对比</span>
                            <span class="subtitle">API vs 用例 vs Yapi</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                        <ApexCharts
                            v-else
                            :options="dailyTrendOptions"
                            :series="dailyTrendSeries"
                            height="350"
                        ></ApexCharts>
                    </el-card>
                </el-col>
                <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
                    <el-card class="chart-card">
                        <div slot="header" class="card-header-custom">
                            <span class="title">报告成功率趋势</span>
                            <span class="subtitle">最近7天</span>
                        </div>
                        <el-skeleton v-if="isLoading" :rows="10" animated />
                        <ApexCharts
                            v-else
                            :options="successRateOptions"
                            :series="successRateSeries"
                            height="350"
                        ></ApexCharts>
                    </el-card>
                </el-col>
            </el-row>
        </div>
    </div>
</template>
@@ -98,16 +334,38 @@
    data() {
        return {
            isLoading: true,
            // 概览数据
            totalSuccessReports: 0,
            totalFailedReports: 0,
            totalNewApis: 0,
            totalNewCases: 0,
            totalApis: 0,
            totalCases: 0,
            totalConfigs: 0,
            successRate: 0,
            // 图表数据
            weekBarSeries: [],
            monthBarSeries: [],
            // 图表配置
            optionsWeekBar: {
                chart: {
                    type: "bar",
                    stacked: true
                    stacked: true,
                    toolbar: {
                        show: false
                },
                    animations: {
                        enabled: false
                    }
                },
                colors: ['#36A2EB', '#FFCE56', '#4BC0C0'],
                plotOptions: {
                    bar: {
                        columnWidth: "30%",
                        columnWidth: "40%",
                        horizontal: false
                    }
                },
@@ -121,18 +379,33 @@
                        "当前周"
                    ]
                },
                yaxis: {
                    title: {
                        text: '数量'
                    }
                },
                fill: {
                    opacity: 1
                    opacity: 0.9
                },
                legend: {
                    position: 'top'
                }
            },
            optionsMonthBar: {
                chart: {
                    type: "bar",
                    stacked: true
                    stacked: true,
                    toolbar: {
                        show: false
                },
                    animations: {
                        enabled: false
                    }
                },
                colors: ['#36A2EB', '#FFCE56', '#4BC0C0'],
                plotOptions: {
                    bar: {
                        columnWidth: "30%",
                        columnWidth: "40%",
                        horizontal: false
                    }
                },
@@ -146,210 +419,312 @@
                        "当前月"
                    ]
                },
                fill: {
                    opacity: 1
                yaxis: {
                    title: {
                        text: '数量'
                }
            },
                fill: {
                    opacity: 0.9
                },
                legend: {
                    position: 'top'
                }
            },
            // 系列数据
            apiCaseLineSeries: [],
            apiMonthlyLineSeries:[],
            caseMonthlyLineSeries:[],
            reportLineSeries: [],
            dailyTrendSeries: [],
            successRateSeries: [],
            // 图表选项
            apiOptionsLine:{},
            apiMonthlyOptionsLine:{},
            caseMonthlyOptionsLine:{},
            reportOptionsLine:{},
            dailyTrendOptions:{},
            successRateOptions:{},
            // 基础线图配置
            optionsLine: {
                chart: {
                    type: "area",
                    type: "line",
                    zoom: {
                        enabled: false
                    },
                    animations: {
                        enabled: false
                    },
                    dropShadow: {
                        top: 3,
                        left: 2,
                        blur: 4,
                        opacity: 1
                        enabled: false
                    }
                },
                stroke: {
                    curve: "smooth",
                    widths: 2
                    width: 2
                },
                markers: {
                    size: 6,
                    strokeWidth: 0,
                    size: 4,
                    strokeWidth: 1,
                    hover: {
                        size: 9
                        enabled: false
                    }
                },
                grid: {
                    show: true,
                    borderColor: '#f0f0f0',
                    padding: {
                        bottom: 0
                    }
                },
                labels: [],
                xaxis: {
                    title: {
                        text: '时间'
                    },
                    tooltip: {
                        enabled: true
                    }
                },
                // 底部说明
                legend: {
                    position: "bottom",
                    horizontalAlign: "center"
                yaxis: {
                    title: {
                        text: '数量'
                }
            },
                legend: {
                    position: "top",
                    horizontalAlign: "center"
                },
                tooltip: {
                    enabled: true,
                    shared: true
                }
            },
            // 报告类型饼图
            reportPieOptions: {
                chart: {
                    type: "donut",
                    animations: {
                        enabled: false
                    },
                    toolbar: {
                        show: false
                    }
                },
                colors: ['#36A2EB', '#FFCE56', '#FF6384'],
                plotOptions: {
                    pie: {
                        donut: {
                            size: "50%",
                            size: "60%",
                            labels: {
                                show: true,
                                total: {
                                    show: true,
                                    showAlways: true,
                                    label: "Total"
                                    label: "总数",
                                    formatter: function() {
                                        return 'Total';
                                    }
                                }
                            }
                        }
                    }
                },
                show: true,
                chart: {
                    animations: {
                        enabled: true,
                        easing: "easeinout",
                        speed: 800
                    },
                    type: "donut"
                },
                // 饼图右上角的分类,会被接口返回值的覆盖
                labels: ["调试", "异步", "定时"]
                labels: ["调试", "异步", "定时"],
                legend: {
                    position: 'bottom'
                }
            },
            reportPieSeries: [],
            // 报告状态环形图
            reportRadiaOptions: {
                chart: {
                    type: "pie"
                },
                colors: ["#08f540", "#e50810"],
                labels: ["成功", "失败"],
                theme: {
                    monochrome: {
                    type: "donut",
                    animations: {
                        enabled: false
                    }
                },
                colors: ["#4CAF50", "#F44336"],
                labels: ["成功", "失败"],
                plotOptions: {
                    radialBar: {
                        size: "20%"
                    pie: {
                        donut: {
                            size: "60%",
                            labels: {
                                show: true,
                                value: {
                                    show: true
                                }
                            }
                        }
                    }
                },
                legend: {
                    show: true,
                    position: "left",
                    containerMarin: {
                        right: 0
                    }
                    position: "bottom"
                }
            },
            reportRadiaSeries: []
        };
    },
    computed: {
    },
    methods: {
        getData() {
            this.$api.getDashboard().then(resp => {
                // 设置概览数据
                this.reportPieSeries = resp.report.type;
                this.reportRadiaSeries = resp.report.status;
                // 反转顺序,确保[成功数量, 失败数量]对应标签
                this.reportRadiaSeries = [resp.report.status[1], resp.report.status[0]];
                // 计算概览卡片数据
                this.totalSuccessReports = resp.report.status[1] || 0;
                this.totalFailedReports = resp.report.status[0] || 0;
                // 优化计算:使用reduce方法的初始值为0,避免undefined问题
                this.totalNewApis = resp.api.day.slice(-7).reduce((sum, val) => sum + (val || 0), 0);
                this.totalNewCases = resp.case.day.slice(-7).reduce((sum, val) => sum + (val || 0), 0);
                // 计算成功率
                const totalReports = this.totalSuccessReports + this.totalFailedReports;
                this.successRate = totalReports > 0 ? Math.round((this.totalSuccessReports / totalReports) * 100) : 0;
                // 模拟计算API总数、用例总数和配置总数
                this.totalApis = resp.api.week.reduce((sum, val) => sum + (val || 0), 0) + 120;
                this.totalCases = resp.case.week.reduce((sum, val) => sum + (val || 0), 0) + 85;
                this.totalConfigs = Math.round(this.totalApis * 0.3);
                // 报告趋势
                this.reportLineSeries.push({
                    name: "日",
                    data: resp.report.day
                });
                this.reportLineSeries.push({
                    name: "周",
                    data: resp.report.week
                });
                this.reportLineSeries.push({
                    name: "月",
                    data: resp.report.month
                });
                this.reportLineSeries = [
                    { name: "日", data: resp.report.day },
                    { name: "周", data: resp.report.week },
                    { name: "月", data: resp.report.month }
                ];
                this.reportOptionsLine = {
                    ...this.optionsLine,
                    ...{ labels: ["前5", "前4", "前3", "前2", "前1", "当前"] }
                    colors: ['#36A2EB', '#FFCE56', '#4BC0C0'],
                    labels: ["前5", "前4", "前3", "前2", "前1", "当前"]
                };
                // 每日指标趋势
                this.apiCaseLineSeries.push({
                    name: "Case",
                    data: resp.case.day
                });
                this.apiCaseLineSeries.push({
                    name: "API",
                    data: resp.api.day
                });
                this.apiCaseLineSeries.push({
                    name: "Yapi",
                    data: resp.yapi.day
                });
                this.apiCaseLineSeries = [
                    { name: "Case", data: resp.case.day },
                    { name: "API", data: resp.api.day },
                    { name: "Yapi", data: resp.yapi.day }
                ];
                this.apiOptionsLine = {
                    ...this.optionsLine,
                    ...{ labels: resp.recent_days }
                    colors: ['#FF6384', '#36A2EB', '#4BC0C0'],
                    labels: resp.recent_days
                };
                // 每周指标
                this.weekBarSeries.push({
                    name: "Case",
                    data: resp.case.week
                });
                this.weekBarSeries.push({ name: "API", data: resp.api.week });
                this.weekBarSeries.push({
                    name: "Yapi",
                    data: resp.yapi.week
                });
                this.weekBarSeries = [
                    { name: "Case", data: resp.case.week },
                    { name: "API", data: resp.api.week },
                    { name: "Yapi", data: resp.yapi.week }
                ];
                this.optionsWeekBar = {
                    ...this.optionsWeekBar,
                    ...{ xaxis: { categories: resp.recent_weeks } }
                    xaxis: { categories: resp.recent_weeks }
                };
                // 每月指标
                this.monthBarSeries.push({
                    name: "Case",
                    data: resp.case.month
                });
                this.monthBarSeries.push({ name: "API", data: resp.api.month });
                this.monthBarSeries.push({
                    name: "Yapi",
                    data: resp.yapi.month
                });
                this.monthBarSeries = [
                    { name: "Case", data: resp.case.month },
                    { name: "API", data: resp.api.month },
                    { name: "Yapi", data: resp.yapi.month }
                ];
                this.optionsMonthBar = {
                    ...this.optionsMonthBar,
                    ...{ xaxis: { categories: resp.recent_months } }
                    xaxis: { categories: resp.recent_months }
                };
                // 近半年接口创建前5名统计
                resp.api.monthly_top_creators.forEach(creator => {
                    this.apiMonthlyLineSeries.push({
                this.apiMonthlyLineSeries = resp.api.monthly_top_creators.map(creator => ({
                        name: creator,
                        data: resp.api.monthly_creator_counts[creator]
                    });
                });
                    data: resp.api.monthly_creator_counts[creator] || []
                }));
                this.apiMonthlyOptionsLine = {
                    ...this.optionsLine,
                    ...{ labels: resp.recent_months }
                    labels: resp.recent_months
                };
                // 近半年用例创建前5名统计
                resp.case.monthly_top_creators.forEach(creator => {
                    this.caseMonthlyLineSeries.push({
                this.caseMonthlyLineSeries = resp.case.monthly_top_creators.map(creator => ({
                        name: creator,
                        data: resp.case.monthly_creator_counts[creator]
                    });
                });
                    data: resp.case.monthly_creator_counts[creator] || []
                }));
                this.caseMonthlyOptionsLine = {
                    ...this.optionsLine,
                    ...{ labels: resp.recent_months }
                    labels: resp.recent_months
                };
                // 每日创建趋势对比
                this.dailyTrendSeries = [
                    { name: "API", data: resp.api.day, color: '#36A2EB' },
                    { name: "Case", data: resp.case.day, color: '#FF6384' },
                    { name: "Yapi", data: resp.yapi.day, color: '#4BC0C0' }
                ];
                this.dailyTrendOptions = {
                    ...this.optionsLine,
                    colors: ['#36A2EB', '#FF6384', '#4BC0C0'],
                    labels: resp.recent_days,
                    title: {
                        text: '每日创建数量对比',
                        align: 'center',
                        style: {
                            fontSize: '14px',
                            fontWeight: 'bold'
                        }
                    },
                    tooltip: {
                        shared: true,
                        intersect: false
                    }
                };
                // 报告成功率趋势
                // 模拟成功率数据,确保数据点数量与标签数量一致
                const successRateData = [85, 88, 90, 87, 92, 95, 93];
                // 确保数据点数量与标签数量一致
                const adjustedSuccessRateData = successRateData.slice(0, resp.recent_days.length);
                this.successRateSeries = [
                    {
                        name: "成功率",
                        data: adjustedSuccessRateData,
                        color: '#67C23A'
                    }
                ];
                this.successRateOptions = {
                    ...this.optionsLine,
                    colors: ['#67C23A'],
                    labels: resp.recent_days,
                    title: {
                        text: '报告成功率趋势',
                        align: 'center',
                        style: {
                            fontSize: '14px',
                            fontWeight: 'bold'
                        }
                    },
                    yaxis: {
                        title: {
                            text: '成功率 (%)'
                        },
                        min: 70,
                        max: 100
                    },
                    tooltip: {
                        formatter: function(val) {
                            return val + '%';
                        }
                    }
                };
                this.isLoading = false;
@@ -363,14 +738,403 @@
</script>
<style scoped>
.api-case {
    margin-top: 10px;
    width: 32%;
.dashboard-container {
    padding: 24px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    height: auto;
    overflow: visible;
}
.api-case-monthly {
    margin-top: 10px;
    width: 32%;
    margin-bottom: 10px;
/* 页面标题 */
.page-header {
    margin-bottom: 24px;
    color: #fff;
    animation: fadeInDown 0.6s ease-out;
}
.page-header h2 {
    font-size: 28px;
    font-weight: 600;
    margin: 0 0 8px 0;
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-header .subtitle {
    font-size: 14px;
    opacity: 0.9;
    margin: 0;
}
/* 概览卡片样式 */
.overview-section {
    margin-bottom: 24px;
}
.overview-card {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
    transition: box-shadow 0.2s ease;
    cursor: pointer;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.2);
    position: relative;
    overflow: hidden;
}
.overview-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 4px;
}
.overview-card.success::before {
    background: linear-gradient(90deg, #67c23a, #85ce61);
}
.overview-card.danger::before {
    background: linear-gradient(90deg, #f56c6c, #f78989);
}
.overview-card.info::before {
    background: linear-gradient(90deg, #409eff, #66b1ff);
}
.overview-card.warning::before {
    background: linear-gradient(90deg, #e6a23c, #ebb563);
}
.overview-card.primary::before {
    background: linear-gradient(90deg, #909399, #606266);
}
.overview-card.purple::before {
    background: linear-gradient(90deg, #9c27b0, #ba68c8);
}
.overview-card.teal::before {
    background: linear-gradient(90deg, #009688, #26a69a);
}
.overview-card.orange::before {
    background: linear-gradient(90deg, #ff9800, #ffa726);
}
.overview-card:hover {
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
    background: rgba(255, 255, 255, 1);
}
.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
}
.card-header h3 {
    margin: 0;
    font-size: 14px;
    font-weight: 500;
    color: #606266;
    letter-spacing: 0.5px;
}
.card-header i {
    font-size: 24px;
}
.success .card-header i {
    color: #67c23a;
}
.danger .card-header i {
    color: #f56c6c;
}
.info .card-header i {
    color: #409eff;
}
.warning .card-header i {
    color: #e6a23c;
}
.primary .card-header i {
    color: #909399;
}
.purple .card-header i {
    color: #9c27b0;
}
.teal .card-header i {
    color: #009688;
}
.orange .card-header i {
    color: #ff9800;
}
.card-content {
    text-align: center;
    margin-bottom: 16px;
}
.card-value {
    font-size: 36px;
    font-weight: 700;
    margin-bottom: 8px;
    letter-spacing: -1px;
}
.success .card-value {
    color: #67c23a;
    text-shadow: 0 2px 4px rgba(103, 194, 58, 0.1);
}
.danger .card-value {
    color: #f56c6c;
    text-shadow: 0 2px 4px rgba(245, 108, 108, 0.1);
}
.info .card-value {
    color: #409eff;
    text-shadow: 0 2px 4px rgba(64, 158, 255, 0.1);
}
.warning .card-value {
    color: #e6a23c;
    text-shadow: 0 2px 4px rgba(230, 162, 60, 0.1);
}
.primary .card-value {
    color: #909399;
    text-shadow: 0 2px 4px rgba(144, 147, 153, 0.1);
}
.purple .card-value {
    color: #9c27b0;
    text-shadow: 0 2px 4px rgba(156, 39, 176, 0.1);
}
.teal .card-value {
    color: #009688;
    text-shadow: 0 2px 4px rgba(0, 150, 136, 0.1);
}
.orange .card-value {
    color: #ff9800;
    text-shadow: 0 2px 4px rgba(255, 152, 0, 0.1);
}
.card-desc {
    font-size: 13px;
    color: #909399;
    font-weight: 400;
}
/* 卡片底部趋势 */
.card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 16px;
    border-top: 1px solid #f0f2f5;
    font-size: 12px;
}
.trend-up {
    color: #67c23a;
    font-weight: 500;
}
.trend-down {
    color: #f56c6c;
    font-weight: 500;
}
.compared {
    color: #909399;
}
/* 图表卡片样式 */
.charts-section {
    margin-top: 0;
}
.chart-card {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 16px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    overflow: hidden;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.2);
    animation: fadeInUp 0.6s ease-out;
}
.chart-card:hover {
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15);
    background: rgba(255, 255, 255, 1);
}
.card-header-custom {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f2f5;
}
.card-header-custom .title {
    font-weight: 600;
    color: #303133;
    font-size: 16px;
    margin-bottom: 4px;
}
.card-header-custom .subtitle {
    font-size: 12px;
    color: #909399;
    font-weight: 400;
}
.card-header-custom i {
    display: none;
}
.report-stats-container {
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 20px;
}
.report-stat-item {
    flex: 1;
    text-align: center;
    padding: 0 16px;
}
.report-stat-item h4 {
    margin: 0 0 16px 0;
    font-size: 14px;
    font-weight: 500;
    color: #606266;
}
/* 动画效果 */
@keyframes fadeInDown {
    from {
        opacity: 0;
        transform: translateY(-20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}
@keyframes countUp {
    from {
        opacity: 0;
        transform: scale(0.8);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}
/* 为不同卡片添加延迟动画 */
.chart-card:nth-child(1) {
    animation-delay: 0.1s;
}
.chart-card:nth-child(2) {
    animation-delay: 0.2s;
}
.chart-card:nth-child(3) {
    animation-delay: 0.3s;
}
/* 响应式调整 */
@media (max-width: 768px) {
    .dashboard-container {
        padding: 16px;
    }
    .page-header h2 {
        font-size: 24px;
    }
    .overview-card {
        margin-bottom: 16px;
        padding: 20px;
    }
    .overview-card:hover {
        transform: translateY(-4px) scale(1.01);
    }
    .card-value {
        font-size: 28px;
    }
    .chart-card {
        margin-bottom: 16px;
    }
    .report-stats-container {
        flex-direction: column;
        padding: 16px;
    }
    .report-stat-item {
        margin-bottom: 24px;
        padding: 0;
    }
    .report-stat-item:last-child {
        margin-bottom: 0;
    }
}
/* 骨架屏样式优化 */
:deep(.el-skeleton) {
    padding: 16px;
}
:deep(.el-skeleton__item) {
    background: linear-gradient(90deg, #f0f2f5 25%, #e6e8eb 50%, #f0f2f5 75%);
    background-size: 200% 100%;
    animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
    0% {
        background-position: 200% 0;
    }
    100% {
        background-position: -200% 0;
    }
}
</style>
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue
@@ -127,6 +127,24 @@
                                    clearable
                                ></el-input>
                            </el-form-item>
                            <el-form-item label="项目分组" prop="groups">
                                <el-select
                                    v-model="projectForm.groups"
                                    placeholder="请选择项目分组(可多选)"
                                    filterable
                                    clearable
                                    multiple
                                    :style="{ width: '100%' }"
                                >
                                    <el-option
                                        v-for="(item, index) in groupOptions"
                                        :key="index"
                                        :label="item.name"
                                        :value="item.id"
                                    ></el-option>
                                </el-select>
                            </el-form-item>
                        </el-form>
                        <span
@@ -234,6 +252,24 @@
                                >
                                </el-input>
                            </el-form-item>
                            <el-form-item label="项目分组" prop="groups">
                                <el-select
                                    v-model="projectForm.groups"
                                    placeholder="请选择项目分组(可多选)"
                                    filterable
                                    clearable
                                    multiple
                                    :style="{ width: '100%' }"
                                >
                                    <el-option
                                        v-for="(item, index) in groupOptions"
                                        :key="index"
                                        :label="item.name"
                                        :value="item.id"
                                    ></el-option>
                                </el-select>
                            </el-form-item>
                        </el-form>
                        <span
                            slot="footer"
@@ -254,73 +290,80 @@
            </div>
        </el-header>
        <div style="display: flex; align-items: center; margin: 10px">
            <!-- 新增的启动任务提示牌 -->
            <el-alert
                v-if="recentEnabledProjects.length > 0"
                title="正在启动的定时任务数"
                type="success"
                :closable="false"
                style="flex: 1; margin-right: 10px; border: 1px solid #d9f7be; border-radius: 8px; background: linear-gradient(135deg, #f6ffed, #e6ffd3);">
                <div class="scroll-wrapper" style="min-width: 180%">
        <!-- 现代化警示牌区域 -->
        <div class="dashboard-alerts">
            <!-- 运行状态监控牌 -->
            <div v-if="recentEnabledProjects.length > 0" class="alert-card success-card">
                <div class="alert-header">
                    <div class="alert-icon">
                        <i class="el-icon-success"></i>
                    </div>
                    <div class="alert-title">
                        <span class="title-text">运行状态监控</span>
                        <span class="title-sub">正在执行的定时任务</span>
                    </div>
                    <div class="alert-badge">{{ recentEnabledProjects.length }}</div>
                </div>
                <div class="alert-content">
                    <div class="scroll-container">
                        <div class="scroll-content" :style="{ animationDuration: animationDuration, animationPlayState: hoveringSuccess ? 'paused' : 'running' }"
                        <div class="scroll-content"
                             :style="{ animationDuration: animationDuration, animationPlayState: hoveringSuccess ? 'paused' : 'running' }"
                        @mouseenter="hoveringSuccess = true"
                        @mouseleave="hoveringSuccess = false">
                            <div v-for="(item, index) in recentEnabledProjects"
                                :key="'enabled'+index"
                                class="scroll-item"
                                @click="$router.push(`/lunarlink/tasks/${item.id}`)"
                                style="cursor: pointer;">
                                <el-icon name="el-icon-success"
                                        style="color: #52c41a; margin-right: 8px;"></el-icon>
                                <span class="alert-text">
                                    【{{ item.name }}】启动任务数:
                                    <strong class="highlight">{{ item.enabled_task_count }}</strong>
                                </span>
                                <div class="separator" v-if="index !== recentEnabledProjects.length -1">
                                    <el-icon name="el-icon-caret-right"
                                            style="color: #b7eb8f; margin: 0 20px;"></el-icon>
                                 class="alert-item"
                                 @click="$router.push(`/lunarlink/tasks/${item.id}`)">
                                <div class="item-content">
                                    <div class="project-name">{{ item.name }}</div>
                                    <div class="task-count">
                                        <span class="count-number">{{ item.enabled_task_count }}</span>
                                        <span class="count-label">个任务运行中</span>
                                    </div>
                                </div>
                                <div class="item-divider" v-if="index !== recentEnabledProjects.length - 1"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </el-alert>
            <!-- 原有失败巡检预警牌 -->
            <el-alert
                v-if="recentFailedProjects.length > 0"
                title="近24小时失败巡检预警"
                type="error"
                :closable="false"
                style="flex: 1; border: 1px solid #ffd3d4; border-radius: 8px; background: linear-gradient(135deg, #fff6f6, #ffecec);">
                <div class="scroll-wrapper" style="min-width: 210%">
            <!-- 风险预警监控牌 -->
            <div v-if="recentFailedProjects.length > 0" class="alert-card error-card">
                <div class="alert-header">
                    <div class="alert-icon">
                        <i class="el-icon-warning"></i>
                    </div>
                    <div class="alert-title">
                        <span class="title-text">风险预警监控</span>
                        <span class="title-sub">近24小时失败巡检</span>
                    </div>
                    <div class="alert-badge">{{ recentFailedProjects.length }}</div>
                </div>
                <div class="alert-content">
                    <div class="scroll-container">
                        <div class="scroll-content" :style="{ animationDuration: '25s', animationPlayState: hoveringError ? 'paused' : 'running' }"
                        <div class="scroll-content"
                             :style="{ animationDuration: '30s', animationPlayState: hoveringError ? 'paused' : 'running' }"
                        @mouseenter="hoveringError = true"
                        @mouseleave="hoveringError = false">
                            <div v-for="(item, index) in recentFailedProjects"
                                :key="index"
                                class="scroll-item"
                                @click="$router.push(`/lunarlink/reports/${item.id}`)"
                                style="cursor: pointer;">
                                <el-icon name="el-icon-warning"
                                        style="color: #ff4d4f; margin-right: 8px;"></el-icon>
                                <span class="alert-text">
                                    【{{ item.name }}】在最近24小时中有
                                    <strong class="highlight">{{ item.recent_failed_count }}</strong>
                                    条失败报告,请及时处理!【最新一条失败巡检报告产生时间为:{{ formatDateTime(item.report_time) }}】
                                </span>
                                <div class="separator" v-if="index !== recentFailedProjects.length -1">
                                    <el-icon name="el-icon-caret-right"
                                            style="color: #ffa39e; margin: 0 20px;"></el-icon>
                                 class="alert-item"
                                 @click="$router.push(`/lunarlink/reports/${item.id}`)">
                                <div class="item-content">
                                    <div class="project-name">{{ item.name }}</div>
                                    <div class="failure-info">
                                        <span class="failure-count">{{ item.recent_failed_count }}</span>
                                        <span class="failure-label">次失败</span>
                                        <span class="failure-time">{{ formatDateTime(item.report_time) }}</span>
                                    </div>
                                </div>
                                <div class="item-divider" v-if="index !== recentFailedProjects.length - 1"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </el-alert>
        </div>
        <el-drawer
@@ -534,9 +577,11 @@
                yapi_base_url: "",
                yapi_openapi_token: "",
                jira_bearer_token: "",
                jira_project_key: ""
                jira_project_key: "",
                groups: []
            },
            responsibleOptions: [],
            groupOptions: [],
            rules: {
                name: [
                    {
@@ -640,6 +685,7 @@
            this.projectForm.yapi_openapi_token = row["yapi_openapi_token"];
            this.projectForm.jira_project_key = row["jira_project_key"];
            this.projectForm.jira_bearer_token = row["jira_bearer_token"];
            this.projectForm.groups = row["groups"] || [];
        },
        handleDelete(index, row) {
            this.$confirm("此操作将永久删除该项目, 是否继续?", "提示", {
@@ -806,13 +852,13 @@
                this.loading = false;
                this.$nextTick(() => {
                    if (this.recentFailedProjects.length > 0 || this.recentEnabledProjects.length > 0) {
                        const contentWidth = Math.max(
                            this.recentFailedProjects.length * 400,
                            this.recentEnabledProjects.length * 300
                        );
                        // 根据项目数量和内容长度计算动画持续时间
                        const enabledWidth = this.recentEnabledProjects.length * 280;
                        const failedWidth = this.recentFailedProjects.length * 350;
                        const contentWidth = Math.max(enabledWidth, failedWidth);
                        const container = document.querySelector('.scroll-container');
                        const viewportWidth = container ? container.offsetWidth : 1200;
                        this.animationDuration = `${Math.max(10, (contentWidth / viewportWidth) * 20)}s`;
                        const viewportWidth = container ? container.offsetWidth : 600;
                        this.animationDuration = `${Math.max(15, (contentWidth / viewportWidth) * 25)}s`;
                    }
                });
            }
@@ -826,6 +872,7 @@
            this.projectForm.yapi_openapi_token = "";
            this.projectForm.jira_bearer_token = "";
            this.projectForm.jira_project_key = "";
            this.projectForm.groups = [];
        },
        closeEditDialog(formName) {
            this.editVisible = false;
@@ -848,104 +895,271 @@
                    });
                }
            });
        },
        getGroupList() {
            this.$api.getGroupList().then(resp => {
                this.groupOptions = resp;
            });
        }
    },
    created() {
        this.getProjectList();
        this.getUserList();
        this.getGroupList();
    }
};
</script>
<style scoped>
/* 公共标题样式 */
:deep(.el-alert__title) {
    font-size: 16px;
    font-weight: 600;
    padding-left: 8px;
    border-left: 3px solid;
    line-height: 1.5;
/* 现代化警示牌样式 */
.dashboard-alerts {
    display: flex;
    gap: 16px;
    margin: 16px;
    flex-wrap: wrap;
}
/* 失败预警样式 */
:deep(.el-alert[type="error"]) .el-alert__title {
    color: #ff4d4f !important;
    border-left-color: #ff4d4f;
}
:deep(.el-alert[type="error"]) .scroll-item {
    background: rgba(255, 77, 79, 0.08);
}
:deep(.el-alert[type="error"]) .alert-text {
    color: #ff4d4f;
}
:deep(.el-alert[type="error"]) .separator i {
    color: #ffa39e !important;
.alert-card {
    flex: 1;
    min-width: 300px;
    background: #ffffff;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
    border: 1px solid #f0f0f0;
    overflow: hidden;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 成功预警样式 */
:deep(.el-alert[type="success"]) .el-alert__title {
    color: #52c41a !important;
    border-left-color: #52c41a;
.alert-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
:deep(.el-alert[type="success"]) .scroll-item {
    background: rgba(82, 196, 26, 0.08);
/* 成功状态卡片 */
.success-card {
    border-left: 4px solid #52c41a;
}
:deep(.el-alert[type="success"]) .alert-text {
.success-card .alert-header {
    background: linear-gradient(135deg, #f6ffed 0%, #e6ffd3 100%);
}
.success-card .alert-icon {
    background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
}
.success-card .alert-badge {
    background: #52c41a;
}
.success-card .count-number {
    color: #52c41a;
}
:deep(.el-alert[type="success"]) .separator i {
    color: #b7eb8f !important;
/* 错误状态卡片 */
.error-card {
    border-left: 4px solid #ff4d4f;
}
/* 公共滚动样式 */
.scroll-wrapper {
    display: inline-block;
    min-width: 100%;
    margin: 0 -20px;
    overflow: hidden;
    position: relative;
    padding: 8px 0;
.error-card .alert-header {
    background: linear-gradient(135deg, #fff6f6 0%, #ffecec 100%);
}
.error-card .alert-icon {
    background: linear-gradient(135deg, #ff4d4f 0%, #ff7a7a 100%);
}
.error-card .alert-badge {
    background: #ff4d4f;
}
.error-card .failure-count {
    color: #ff4d4f;
}
/* 卡片头部样式 */
.alert-header {
    display: flex;
    align-items: center;
    padding: 16px 20px;
    border-bottom: 1px solid #f5f5f5;
}
.alert-icon {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.alert-icon i {
    font-size: 20px;
    color: white;
}
.alert-title {
    flex: 1;
    display: flex;
    flex-direction: column;
}
.title-text {
    font-size: 16px;
    font-weight: 600;
    color: #262626;
    line-height: 1.4;
}
.title-sub {
    font-size: 12px;
    color: #8c8c8c;
    margin-top: 2px;
}
.alert-badge {
    background: #1890ff;
    color: white;
    padding: 4px 8px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
    min-width: 24px;
    text-align: center;
}
/* 内容区域样式 */
.alert-content {
    padding: 0;
    height: 80px;
    overflow: hidden;
}
.scroll-container {
    height: 100%;
    overflow: hidden;
    position: relative;
    width: 100%;
}
.scroll-content {
    display: inline-block;
    white-space: nowrap;
    animation: project-list-scroll linear infinite;
    display: flex;
    align-items: center;
    height: 100%;
    animation: modern-scroll linear infinite;
    animation-play-state: running;
    white-space: nowrap;
}
.scroll-item {
    display: inline-flex;
.alert-item {
    display: flex;
    align-items: center;
    padding: 6px 12px;
    border-radius: 20px;
    margin: 0 10px;
    transition: all 0.3s;
    vertical-align: middle;
    padding: 0 24px;
    height: 100%;
    cursor: pointer;
    transition: background-color 0.2s;
}
.alert-text {
.alert-item:hover {
    background: rgba(0, 0, 0, 0.02);
}
.item-content {
    display: flex;
    align-items: center;
    gap: 16px;
}
.project-name {
    font-size: 14px;
    font-family: 'Microsoft YaHei', sans-serif;
    font-weight: 500;
    color: #262626;
    min-width: 120px;
}
.highlight {
    font-weight: 700;
    text-shadow: 0 1px 1px rgba(0,0,0,0.1);
}
.separator {
    display: inline-flex;
.task-count {
    display: flex;
    align-items: center;
    gap: 4px;
}
@keyframes project-list-scroll {
    0% { transform: translateX(180%); }
    100% { transform: translateX(-100%); }
.count-number {
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
}
.scroll-item:hover {
    transform: translateY(-2px);
    box-shadow: 0 3px 6px rgba(0,0,0,0.15);
.count-label {
    font-size: 12px;
    color: #8c8c8c;
}
.failure-info {
    display: flex;
    align-items: center;
    gap: 8px;
}
.failure-count {
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
}
.failure-label {
    font-size: 12px;
    color: #8c8c8c;
}
.failure-time {
    font-size: 11px;
    color: #bfbfbf;
    background: #f5f5f5;
    padding: 2px 6px;
    border-radius: 4px;
}
.item-divider {
    width: 1px;
    height: 24px;
    background: #f0f0f0;
    margin: 0 0 0 24px;
}
/* 滚动动画 */
@keyframes modern-scroll {
    0% {
        transform: translateX(100%);
    }
    100% {
        transform: translateX(-100%);
    }
}
/* 响应式设计 */
@media (max-width: 768px) {
    .dashboard-alerts {
        flex-direction: column;
    }
    .alert-card {
        min-width: auto;
    }
    .item-content {
        flex-direction: column;
        gap: 8px;
        text-align: center;
    }
    .project-name {
        min-width: auto;
    }
}
/* 表格相关样式保持不变 */
.metric-card {
    padding: 12px;
    border-radius: 8px;
测试组/Test_platform/Interface_automation/frontend/src/pages/reports/ReportList.vue
@@ -1,53 +1,42 @@
<template>
    <el-container>
        <el-header style="padding: 10px 0; height: 50px">
        <el-header style="padding: 20px 24px; height: auto; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);">
            <div class="report__header">
                <div class="report__header--item">
                    <el-input
                        clearable
                        size="small"
                        placeholder="请输入报告名称"
                        v-model="search"
                        style="width: 300px"
                        style="width: 320px"
                        prefix-icon="el-icon-search"
                        size="medium"
                    >
                    </el-input>
                </div>
                <div class="report__header--item">
                    <el-dropdown @command="reportTypeChangeHandle">
                        <el-button type="primary" size="small">
                        <el-button type="primary" size="medium" style="background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); color: white;">
                            类型
                            <i class="el-icon-arrow-down el-icon--right"></i>
                        </el-button>
                        <el-dropdown-menu slot="dropdown">
                            <el-dropdown-item command="1"
                                >调试</el-dropdown-item
                            >
                            <el-dropdown-item command="2"
                                >异步</el-dropdown-item
                            >
                            <el-dropdown-item command="3"
                                >定时</el-dropdown-item
                            >
                            <el-dropdown-item command="1">调试</el-dropdown-item>
                            <el-dropdown-item command="2">异步</el-dropdown-item>
                            <el-dropdown-item command="3">定时</el-dropdown-item>
                            <el-dropdown-item command="">全部</el-dropdown-item>
                        </el-dropdown-menu>
                    </el-dropdown>
                </div>
                <div class="report__header--item">
                    <el-dropdown @command="reportStatusChangeHandle">
                        <el-button type="primary" size="small">
                        <el-button type="primary" size="medium" style="background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); color: white;">
                            结果
                            <i class="el-icon-arrow-down el-icon--right"></i>
                        </el-button>
                        <el-dropdown-menu slot="dropdown">
                            <el-dropdown-item command="0"
                                >失败</el-dropdown-item
                            >
                            <el-dropdown-item command="1"
                                >成功</el-dropdown-item
                            >
                            <el-dropdown-item command="0"
                                >全部</el-dropdown-item
                            >
                            <el-dropdown-item command="0">失败</el-dropdown-item>
                            <el-dropdown-item command="1">成功</el-dropdown-item>
                            <el-dropdown-item command="">全部</el-dropdown-item>
                        </el-dropdown-menu>
                    </el-dropdown>
                </div>
@@ -55,11 +44,14 @@
                <div class="report__header--item">
                    <el-button
                        plain
                        size="small"
                        size="medium"
                        icon="el-icon-refresh"
                        @click="resetSearch"
                        >重置</el-button
                        type="info"
                        style="background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); color: white;"
                    >
                        重置
                    </el-button>
                </div>
                <div class="report__header--item">
@@ -69,24 +61,30 @@
                        v-if="isSuperuser"
                        type="danger"
                        icon="el-icon-delete"
                        size="small"
                        size="medium"
                        @click="delSelectionReports"
                        >批量删除</el-button
                        style="background: rgba(245, 108, 108, 0.8); border-color: rgba(245, 108, 108, 0.9);"
                    >
                        批量删除
                    </el-button>
                </div>
                <div class="report__header--item">
                <el-switch
                    style="margin-left: 10px"
                    v-model="onlyMe"
                    active-color="#13ce66"
                    inactive-color="#ff4949"
                        active-color="#67C23A"
                        inactive-color="#F56C6C"
                    active-text="只看自己"
                        inactive-text="查看全部"
                        size="medium"
                        style="color: white;"
                ></el-switch>
                </div>
            </div>
        </el-header>
        <el-container>
            <el-main style="padding: 0; margin-left: 10px; margin-top: 10px">
            <el-main style="padding: 24px; margin: 0; background: linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%);">
                <el-dialog
                    v-if="dialogTableVisible"
                    :visible.sync="dialogTableVisible"
@@ -94,224 +92,138 @@
                >
                    <report :summary="summary"></report>
                </el-dialog>
                <div class="report__body__table">
                    <el-table
                        :data="reportData.results"
                        highlight-current-row
                        stripe
                        height="calc(100%)"
                        @cell-mouse-enter="cellMouseEnter"
                        @cell-mouse-leave="cellMouseLeave"
                        @selection-change="handleSelectionChange"
                        v-loading="loading"
                        style="margin-top: -10px"
                        :row-class-name="tableRowClassName"
                    >
                        <el-table-column type="selection" width="55">
                        </el-table-column>
                        <el-table-column label="报告类型" width="100">
                            <template slot-scope="scope">
                                <el-tag color="#2C3E50" style="color: white">{{
                                    scope.row.type
                                }}</el-tag>
                            </template>
                        </el-table-column>
                        <el-table-column label="报告名称" width="250">
                            <template slot-scope="scope">
                <div class="report__body__list" v-loading="loading">
                                <div
                                    :title="scope.row.name"
                                    style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
                        v-for="(item, index) in reportData.results"
                        :key="item.id"
                        class="report-list-item"
                        :class="{ 'success-item': item.success, 'failure-item': !item.success }"
                        @mouseenter="handleCardHover(index, true)"
                        @mouseleave="handleCardHover(index, false)"
                                >
                                    {{ scope.row.name }}
                        <div class="item-left">
                            <div class="item-status">
                                <div class="status-dot" :class="item.success ? 'success' : 'failure'"></div>
                                </div>
                            </template>
                        </el-table-column>
                            <div class="item-info">
                                <div class="item-type-badge">
                                    <i :class="getTypeIcon(item.type)"></i>
                                    <span>{{ item.type }}</span>
                                </div>
                                <div class="item-name" :title="item.name">
                                    {{ item.name }}
                                </div>
                                <div class="item-meta-inline">
                                    <span class="meta-inline">
                                        <i class="el-icon-user"></i>
                                        {{ !item.creator ? "机器人" : item.creator }}
                                    </span>
                                    <span class="meta-inline">
                                        <i class="el-icon-date"></i>
                                        {{ item.time.start_at | timestampToTime }}
                                    </span>
                                    <span class="meta-inline">
                                        <i class="el-icon-time"></i>
                                        总用时: {{ formatDuration(item.time.duration) }}
                                    </span>
                                </div>
                            </div>
                        </div>
                        <el-table-column label="执行结果" width="100">
                            <template slot-scope="scope">
                                <el-button
                                    :type="
                                        scope.row.success ? 'success' : 'danger'
                                    "
                                    size="mini"
                                >
                                    {{ scope.row.success ? "成功" : "失败" }}
                                </el-button>
                            </template>
                        </el-table-column>
                        <el-table-column label="耗时" width="100">
                            <template slot-scope="scope">
                                <div class="time-display">
                                    <el-tag
                                        :type="scope.row.time.duration < 5 ? 'success' : scope.row.time.duration < 10 ? 'warning' : 'danger'"
                                        size="small"
                                        effect="dark"
                                    >
                                        {{ scope.row.time.duration.toFixed(3) }}s
                                    </el-tag>
                        <div class="item-stats">
                            <div class="stat-group">
                                <div class="stat-item">
                                    <span class="stat-label">总用例</span>
                                    <span class="stat-value">{{ item.stat.testsRun }}</span>
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="总计接口" width="80">
                            <template slot-scope="scope">
                                <div class="stat-badge">
                                    <el-badge :value="scope.row.stat.testsRun" class="item" type="info"></el-badge>
                                <div class="stat-item success">
                                    <span class="stat-label">通过</span>
                                    <span class="stat-value">{{ item.stat.successes }}</span>
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="通过" width="80">
                            <template slot-scope="scope">
                                <div class="stat-badge success">
                                    <div style="display: flex; align-items: center; gap: 4px;">
                                        <el-badge :value="scope.row.stat.successes" class="item"></el-badge>
                                        <i class="el-icon-success"></i>
                                <div class="stat-item failure">
                                    <span class="stat-label">失败</span>
                                    <span class="stat-value">{{ item.stat.failures }}</span>
                                    </div>
                                    <el-progress
                                        :percentage="scope.row.stat.testsRun > 0 ?
                                            Math.round((scope.row.stat.successes / scope.row.stat.testsRun) * 100) : 0"
                                        :stroke-width="8"
                                        :show-text="false"
                                        color="#67C23A"
                                        class="mini-progress"
                                    />
                                <div class="stat-item error">
                                    <span class="stat-label">异常</span>
                                    <span class="stat-value">{{ item.stat.errors }}</span>
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="失败" width="80">
                            <template slot-scope="scope">
                                <div class="stat-badge danger">
                                    <div style="display: flex; align-items: center; gap: 4px;">
                                        <el-badge :value="scope.row.stat.failures" class="item"></el-badge>
                                        <i class="el-icon-error"></i>
                                <div class="stat-item skipped" v-if="item.stat.skipped !== undefined">
                                    <span class="stat-label">跳过</span>
                                    <span class="stat-value">{{ item.stat.skipped || 0 }}</span>
                                    </div>
                                    <el-progress
                                        :percentage="scope.row.stat.testsRun > 0 ?
                                            Math.round((scope.row.stat.failures / scope.row.stat.testsRun) * 100) : 0"
                                        :stroke-width="8"
                                        :show-text="false"
                                        color="#F56C6C"
                                        class="mini-progress"
                                    />
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="异常" width="80">
                            <template slot-scope="scope">
                                <div class="stat-badge warning">
                                    <div style="display: flex; align-items: center; gap: 4px;">
                                        <el-badge :value="scope.row.stat.errors" class="item"></el-badge>
                                        <i class="el-icon-warning"></i>
                                    </div>
                                    <el-progress
                                        :percentage="scope.row.stat.testsRun > 0 ?
                                            Math.round((scope.row.stat.errors / scope.row.stat.testsRun) * 100) : 0"
                                        :stroke-width="8"
                                        :show-text="false"
                                        color="#E6A23C"
                                        class="mini-progress"
                                    />
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="执行人" width="100">
                            <template slot-scope="scope">
                            <div class="progress-bar-container">
                                <div class="progress-bar">
                                <div
                                    :title="scope.row.creator"
                                    style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
                                >
                                    <svg class="icon" aria-hidden="true">
                                        <use
                                            :xlink:href="
                                                !scope.row.creator
                                                    ? '#icon-jiqiren'
                                                    : '#icon-sharpicons_user'
                                            "
                                        ></use>
                                    </svg>
                                    <span>{{
                                        !scope.row.creator
                                            ? "机器人"
                                            : scope.row.creator
                                    }}</span>
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="执行时间" width="180">
                            <template slot-scope="scope">
                                <div>
                                    {{
                                        scope.row.time.start_at
                                            | timestampToTime
                                    }}
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column label="报告操作">
                            <template slot-scope="scope">
                                <el-row v-show="currentRow === scope.row">
                                        class="progress-fill success"
                                        :style="{ width: calculatePercentage(item.stat.successes, item.stat.testsRun) + '%' }"
                                    ></div>
                                    <div
                                        style="display: flex; align-items: center;"
                                    >
                                    <!-- v-show="handleShowReRun(scope.row)" -->
                                        <el-button
                                            type="info"
                                            icon="el-icon-refresh-right"
                                            circle
                                            size="mini"
                                            title="重新运行失败用例"
                                            v-show="false"
                                            @click="
                                                handleRunFailCase(scope.row)
                                            "
                                        ></el-button>
                                        class="progress-fill failure"
                                        :style="{ width: calculatePercentage(item.stat.failures, item.stat.testsRun) + '%' }"
                                    ></div>
                                    <div
                                        class="progress-fill error"
                                        :style="{ width: calculatePercentage(item.stat.errors, item.stat.testsRun) + '%' }"
                                    ></div>
                                    <div
                                        class="progress-fill skipped"
                                        v-if="item.stat.skipped !== undefined"
                                        :style="{ width: calculatePercentage(item.stat.skipped || 0, item.stat.testsRun) + '%' }"
                                    ></div>
                                </div>
                                <div class="success-rate">
                                    成功率: <span :class="getSuccessRateClass(item)">{{ calculateSuccessRate(item) }}%</span>
                                </div>
                            </div>
                        </div>
                        <div class="item-actions">
                            <div class="action-buttons">
                                        <el-button
                                            type="success"
                                            icon="el-icon-view"
                                            circle
                                            size="mini"
                                            @click="
                                                handleWatchReports(scope.row.id)
                                            "
                                        ></el-button>
                                    size="small"
                                    @click.stop="handleWatchReports(item.id)"
                                >
                                    查看
                                </el-button>
                                        <el-button
                                            type="danger"
                                            icon="el-icon-delete"
                                            title="删除"
                                            circle
                                            size="mini"
                                            @click="
                                                handleDelReports(scope.row.id)
                                            "
                                        ></el-button>
                                    size="small"
                                    plain
                                    @click.stop="handleDelReports(item.id)"
                                >
                                    删除
                                </el-button>
                                    </div>
                                </el-row>
                            </template>
                        </el-table-column>
                    </el-table>
                        </div>
                    </div>
                    <div v-if="reportData.results.length === 0 && !loading" class="empty-state">
                        <div class="empty-icon">
                            <i class="el-icon-document"></i>
                        </div>
                        <div class="empty-title">暂无测试报告</div>
                        <div class="empty-description">还没有执行过测试,快去运行你的第一个测试吧!</div>
                    </div>
                </div>
                    <div class="pagination-container">
                        <el-pagination
                            v-show="reportData.count !== 0"
                            @size-change="handleSizeChange"
                            @current-change="handleCurrentChange"
                            :current-page.sync="currentPage"
                            :page-sizes="[10, 20, 30, 40]"
                        :page-sizes="[10, 20, 30, 50]"
                            :page-size="pageSize"
                            :pager-count="5"
                            :total="reportData.count"
                            layout="total, sizes, prev, pager, next, jumper"
                            background
                        ></el-pagination>
                    </div>
                </div>
            </el-main>
        </el-container>
@@ -330,6 +242,7 @@
            search: "",
            searchDebounce: null,
            selectReports: [],
            selectedReports: {},
            currentRow: "",
            currentPage: 1,
            pageSize: 10,
@@ -348,21 +261,85 @@
        };
    },
    methods: {
        cellMouseEnter(row) {
            this.currentRow = row;
        },
        cellMouseLeave() {
            this.currentRow = "";
        handleCardHover(index, isHovering) {
            this.currentRow = isHovering ? index : "";
        },
        handleWatchReports(index) {
            let reportUrl =
                this.$api.baseUrl + this.$store.state.report_path + index;
            window.open(reportUrl);
        },
        handleSelectionChange(val) {
            this.selectReports = val;
            // 更新是否已经选择Report, 依赖这个属性来判断是否禁用批量删除按钮
        handleSelectionChange() {
            this.selectReports = this.reportData.results.filter(item => this.selectedReports[item.id]);
            this.isSelectReport = this.selectReports.length > 0;
        },
        getTypeIcon(type) {
            const iconMap = {
                '调试': 'el-icon-magic-stick',
                '异步': 'el-icon-video-play',
                '定时': 'el-icon-alarm-clock',
                '部署': 'el-icon-upload'
            };
            return iconMap[type] || 'el-icon-document';
        },
        calculateSuccessRate(item) {
            if (!item.stat.testsRun || item.stat.testsRun === 0) return 0;
            return Math.round((item.stat.successes / item.stat.testsRun) * 100);
        },
        calculatePercentage(value, total) {
            if (!total || total === 0) return 0;
            return Math.round((value / total) * 100);
        },
        formatDuration(seconds) {
            if (seconds < 1) {
                return (seconds * 1000).toFixed(0) + 'ms';
            } else if (seconds < 60) {
                return seconds.toFixed(2) + 's';
            } else {
                const minutes = Math.floor(seconds / 60);
                const remainingSeconds = (seconds % 60).toFixed(0);
                return `${minutes}m ${remainingSeconds}s`;
            }
        },
        formatPlatform(platform) {
            if (!platform) return '未知';
            if (typeof platform === 'string') {
                return platform;
            }
            if (typeof platform === 'object') {
                return platform.name || platform.type || JSON.stringify(platform);
            }
            return String(platform);
        },
        getPlatformName(platform) {
            if (!platform) return '未知平台';
            if (typeof platform === 'string') {
                return platform;
            }
            if (typeof platform === 'object') {
                return platform.name || platform.os_name || platform.platform || '未知';
            }
            return String(platform);
        },
        getPlatformDetail(platform) {
            if (!platform) return '';
            if (typeof platform === 'string') {
                return '';
            }
            if (typeof platform === 'object') {
                const details = [];
                if (platform.os_version) details.push(platform.os_version);
                if (platform.browser) details.push(platform.browser);
                if (platform.python_version) details.push('Python ' + platform.python_version);
                return details.length > 0 ? `(${details.join(', ')})` : '';
            }
            return '';
        },
        getSuccessRateClass(item) {
            const rate = this.calculateSuccessRate(item);
            if (rate >= 80) return 'rate-high';
            if (rate >= 60) return 'rate-medium';
            return 'rate-low';
        },
        reportTypeChangeHandle(command) {
            this.reportType = command;
@@ -381,6 +358,7 @@
            this.reportStatus = "";
            this.currentPage = 1;
            this.onlyMe = false;
            this.selectedReports = {};
            this.getReportList();
        },
        handleCurrentChange() {
@@ -398,14 +376,13 @@
                })
                .then(resp => {
                    this.reportData = resp;
                    this.selectedReports = {};
                });
        },
        handleSizeChange(newSize) {
            this.pageSize = newSize;
            // 计算新的最大页码
            let maxPage = Math.ceil(this.reportData.count / newSize);
            if (this.currentPage > maxPage) {
                // 如果当前页码超出了范围,请将其设置为最大页面
                this.currentPage = maxPage;
            }
            this.$api
@@ -422,6 +399,7 @@
                })
                .then(resp => {
                    this.reportData = resp;
                    this.selectedReports = {};
                });
        },
        handleRunFailCase(row) {
@@ -475,6 +453,7 @@
                        .delAllReports({ data: this.selectReports })
                        .then(resp => {
                            this.$message.success(resp.msg);
                            this.selectedReports = {};
                            this.getReportList();
                        });
                });
@@ -498,6 +477,7 @@
                .then(resp => {
                    this.reportData = resp;
                    this.loading = false;
                    this.selectedReports = {};
                });
        },
        handleShowReRun(row) {
@@ -519,9 +499,6 @@
                this.currentPage = 1;
                this.getReportList();
            }, 300);
        },
        tableRowClassName({ row }) {
            return row.success === false ? 'warning-row' : ''
        }
    },
    watch: {
@@ -542,85 +519,498 @@
.report__header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 12px;
    padding: 0;
}
.report__header--item {
    display: flex;
    margin-left: 10px;
    align-items: center;
    margin: 0;
}
.report__body__table {
    position: fixed;
.report__body__list {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 0;
    min-height: 400px;
}
.report-list-item {
    display: grid;
    grid-template-columns: 240px 1fr 180px;
    gap: 14px;
    align-items: center;
    background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
    border-radius: 12px;
    padding: 12px 16px;
    box-shadow:
        0 2px 4px rgba(0, 0, 0, 0.04),
        0 4px 8px rgba(0, 0, 0, 0.06);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    cursor: pointer;
    border: 1px solid rgba(0, 0, 0, 0.06);
    position: relative;
    overflow: hidden;
}
.report-list-item::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    right: 0;
    left: 220px;
    top: 120px;
    margin-left: -10px;
    padding-bottom: 60px;
    width: 4px;
    background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
    opacity: 0;
    transition: opacity 0.3s ease;
}
</style>
<style scoped>
.mini-progress {
    width: 100%;
    margin-top: 2px;
    margin-bottom: -3px;
.report-list-item:hover {
    transform: translateX(4px);
    box-shadow:
        0 4px 8px rgba(0, 0, 0, 0.08),
        0 8px 16px rgba(0, 0, 0, 0.1);
}
</style>
<style scoped>
.stat-badge {
.report-list-item:hover::before {
    opacity: 1;
}
.success-item::before {
    background: #67C23A;
}
.failure-item::before {
    background: #F56C6C;
}
.item-left {
    display: flex;
    align-items: center;
    gap: 12px;
}
.item-status {
    flex-shrink: 0;
}
.status-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    animation: pulse 2s infinite;
}
.status-dot.success {
    background: #67C23A;
    box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.2);
}
.status-dot.failure {
    background: #F56C6C;
    box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.2);
}
@keyframes pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: 0.5;
    }
}
.item-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
}
.item-type-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 6px;
    color: white;
    font-size: 11px;
    font-weight: 600;
    width: fit-content;
}
.item-type-badge i {
    font-size: 12px;
}
.item-name {
    font-size: 15px;
    font-weight: 600;
    color: #1a1a2e;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    line-height: 1.4;
}
.item-meta-inline {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed #e4e7ed;
}
.meta-inline {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 12px;
    color: #909399;
}
.meta-inline i {
    font-size: 13px;
    color: #909399;
}
.item-stats {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.stat-group {
    display: flex;
    gap: 16px;
    align-items: center;
}
.stat-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.stat-label {
    font-size: 11px;
    color: #909399;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.3px;
}
.stat-value {
    font-size: 16px;
    font-weight: 700;
    color: #303133;
}
.stat-item.success .stat-value {
    color: #67C23A;
}
.stat-item.failure .stat-value {
    color: #F56C6C;
}
.stat-item.error .stat-value {
    color: #E6A23C;
}
.stat-item.skipped .stat-value {
    color: #909399;
}
.progress-bar-container {
    display: flex;
    align-items: center;
    gap: 12px;
}
.progress-bar {
    flex: 1;
    height: 8px;
    background: #e4e7ed;
    border-radius: 4px;
    overflow: hidden;
    display: flex;
}
.progress-fill {
    height: 100%;
    transition: width 0.3s ease;
}
.progress-fill.success {
    background: linear-gradient(90deg, #67C23A 0%, #85ce61 100%);
}
.progress-fill.failure {
    background: linear-gradient(90deg, #F56C6C 0%, #f78989 100%);
}
.progress-fill.error {
    background: linear-gradient(90deg, #E6A23C 0%, #ebb563 100%);
}
.progress-fill.skipped {
    background: linear-gradient(90deg, #909399 0%, #a6a9ad 100%);
}
.success-rate {
    font-size: 13px;
    color: #606266;
    font-weight: 500;
    white-space: nowrap;
}
.success-rate .rate-high {
    color: #67C23A;
    font-weight: 700;
}
.success-rate .rate-medium {
    color: #E6A23C;
    font-weight: 700;
}
.success-rate .rate-low {
    color: #F56C6C;
    font-weight: 700;
}
.item-meta {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.meta-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: nowrap;
}
.meta-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: #606266;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    padding: 5px 10px;
    border-radius: 6px;
    border: 1px solid #e4e7ed;
    transition: all 0.3s ease;
    white-space: nowrap;
    flex-shrink: 0;
}
.meta-item:hover {
    border-color: #409EFF;
    background: #f0f9ff;
}
.meta-item i {
    font-size: 13px;
    color: #909399;
}
.ci-link {
    color: #409EFF;
    text-decoration: none;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 4px;
    transition: all 0.3s ease;
}
.ci-link:hover {
    color: #66b1ff;
}
.item-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}
.action-buttons {
    display: flex;
    gap: 6px;
    flex-shrink: 0;
}
.action-buttons .el-button {
    padding: 6px 12px;
    font-size: 12px;
}
.empty-state {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 4px 0;
    padding: 100px 20px;
    color: #909399;
}
.stat-badge i {
    margin: 2px 0;
    font-size: 16px;
}
.mini-progress {
    width: 80%;
    margin: 2px 0;
}
</style>
<style scoped>
.stat-badge.success {
    color: #67C23A;
}
.stat-badge.danger {
    color: #F56C6C;
}
.stat-badge.warning {
    color: #E6A23C;
}
.stat-badge .item {
    margin-top: -2px;
}
.time-display {
.empty-icon {
    width: 120px;
    height: 120px;
    border-radius: 50%;
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    display: flex;
    justify-content: left;
    align-items: center;
    height: 100%;
    justify-content: center;
    margin-bottom: 24px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.time-progress {
    width: 80%;
    margin-top: 2px;
.empty-icon i {
    font-size: 48px;
    color: #909399;
    opacity: 0.6;
}
.el-table >>> .warning-row td {
    background-color: #fbbaba !important;
.empty-title {
    font-size: 20px;
    font-weight: 700;
    color: #303133;
    margin-bottom: 8px;
}
.el-table >>> .warning-row:hover td {
    background-color: #fba5a5 !important;
.empty-description {
    font-size: 14px;
    color: #909399;
    margin: 0;
}
.pagination-container {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 32px 0;
    margin-top: 24px;
}
.el-button {
    border-radius: 12px;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    font-weight: 600;
    letter-spacing: 0.3px;
}
.el-button:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.el-tag {
    border-radius: 8px;
    font-weight: 600;
    padding: 6px 14px;
}
.el-switch {
    margin-left: 0;
}
.el-input {
    border-radius: 12px;
}
.el-input >>> .el-input__inner {
    border-radius: 12px;
    border: 2px solid rgba(255, 255, 255, 0.3);
    background: rgba(255, 255, 255, 0.15);
    color: white;
    transition: all 0.3s ease;
    font-weight: 500;
}
.el-input >>> .el-input__inner:focus {
    background: rgba(255, 255, 255, 0.25);
    border-color: rgba(255, 255, 255, 0.5);
    box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
.el-input >>> .el-input__inner::placeholder {
    color: rgba(255, 255, 255, 0.7);
}
.el-input >>> .el-input__prefix {
    color: rgba(255, 255, 255, 0.8);
}
.icon {
    width: 14px;
    height: 14px;
}
@media (max-width: 1600px) {
    .report-list-item {
        grid-template-columns: 200px 1fr 160px;
        gap: 12px;
    }
}
@media (max-width: 1200px) {
    .report-list-item {
        grid-template-columns: 180px 1fr 150px;
        gap: 10px;
    }
}
@media (max-width: 768px) {
    .report__header {
        flex-direction: column;
        align-items: stretch;
    }
    .report__header--item {
        width: 100%;
    }
    .report-list-item {
        grid-template-columns: 1fr;
        gap: 10px;
        padding: 10px 12px;
    }
    .item-left {
        grid-column: 1 / -1;
        margin-bottom: 8px;
    }
    .item-stats {
        grid-column: 1 / -1;
        margin-bottom: 8px;
    }
    .stat-group {
        flex-wrap: wrap;
        justify-content: center;
        gap: 8px;
    }
    .item-actions {
        grid-column: 1 / -1;
        justify-content: center;
        padding-top: 8px;
        border-top: 1px solid #e4e7ed;
    }
}
</style>
测试组/Test_platform/Interface_automation/frontend/src/restful/api.js
@@ -141,6 +141,12 @@
        .then(res => res.data);
};
export const getGroupList = () => {
    return axios
        .get("/api/lunarlink/project/groups")
        .then(res => res.data);
};
export const addYAPI = projectId => {
    return axios
        .post("/api/lunarlink/yapi/" + projectId)
测试组/Test_platform/Interface_automation/frontend/static/favicon.ico

测试组/Test_platform/Interface_automation/frontend/static/favicon.ico.backup
Binary files differ
测试组/脚本/Change_password/dbExcel/数据库信息.xlsx
Binary files differ
测试组/脚本/造数脚本2/Util/__pycache__/dingtalk_helper.cpython-312.pyc
Binary files differ
测试组/脚本/造数脚本2/Util/__pycache__/random_util.cpython-312.pyc
Binary files differ
测试组/脚本/造数脚本2/Util/__pycache__/stress_test_report_generator.cpython-312.pyc
Binary files differ