接口自动化平台优化登录页面和首页;
项目看板增加多个统计数据和详细数据信息,看板布局和样式优化
1 files added
27 files modified
| | |
| | | # 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 |
| | | |
| | |
| | | ## 平台简介 |
| | | |
| | | 基于HttpRunner + Django + Vue + Element UI 的接口自动化测试平台,生产可用。 |
| | | |
| | | ## 技术栈 |
| | |
| | | ``` |
| | | |
| | | ## Docker构建 |
| | | 请参考文档[Docker构建](deployment/README.md) |
| | | |
| | |
| | | |
| | | 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 |
| | | |
| | |
| | | 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 |
| | |
| | | "api_cover_rate", |
| | | "jira_project_key", |
| | | "jira_bearer_token", |
| | | "groups", |
| | | ] |
| | | |
| | | def get_updater_name(self, obj): |
| | |
| | | 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) |
| | | |
| | |
| | | ), |
| | | 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()), |
| | |
| | | from typing import Dict |
| | | from django.conf import settings |
| | | import time |
| | | from datetime import datetime |
| | | |
| | | |
| | | def parse_message(summary: Dict, **kwargs): |
| | |
| | | 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, |
| | |
| | | "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, |
| | | } |
| | | |
| | | |
| | |
| | | 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, |
| | | ): |
| | | """ |
| | | 定制邮件报告消息模板(高级优化版) |
| | |
| | | :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; |
| | |
| | | .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> |
| | |
| | | <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> |
| | |
| | | |
| | | 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()) |
| | |
| | | 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": { |
| | |
| | | "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)}") |
| | |
| | | """ |
| | | 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 |
| | |
| | | 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): |
| | | """项目看板""" |
| | |
| | | # 包含今天的前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) |
| | |
| | | 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 |
| | |
| | | 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) |
| | |
| | | import time |
| | | import unittest |
| | | from base64 import b64encode |
| | | try: |
| | | from collections.abc import Iterable |
| | | except ImportError: |
| | | from collections import Iterable |
| | | from datetime import datetime |
| | | |
| | |
| | | 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 && \ |
| | |
| | | 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 && \ |
| | |
| | | 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 && \ |
| | |
| | | privileged: true |
| | | restart: always |
| | | ports: |
| | | - "3306:3306" |
| | | - "3307:3307" |
| | | expose: |
| | | - 3306 |
| | | - 3307 |
| | | environment: |
| | | MYSQL_DATABASE: "lunarlink" |
| | | MYSQL_ROOT_PASSWORD: "root" |
| | |
| | | @click="handleRunCode" |
| | | >在线运行</el-button |
| | | > |
| | | <el-button |
| | | round |
| | | icon="el-icon-document-copy" |
| | | type="success" |
| | | size="small" |
| | | @click="handleImportCode" |
| | | >插入其他项目代码</el-button |
| | | > |
| | | </div> |
| | | </el-header> |
| | | |
| | |
| | | > |
| | | <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> |
| | | |
| | |
| | | editor: null, |
| | | timeStamp: "", |
| | | isShowDebug: false, |
| | | importDialogVisible: false, |
| | | projectList: [], |
| | | selectedProjectId: "", |
| | | selectedProjectCode: "", |
| | | options: { |
| | | selectOnLineNumbers: false, |
| | | scrollbar: { |
| | |
| | | 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: { |
| | |
| | | <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 |
| | |
| | | @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" |
| | |
| | | :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> |
| | | |
| | |
| | | <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的间距 */ |
| | | } |
| | |
| | | <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"></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"></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"></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"></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"></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"></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> |
| | |
| | | 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 |
| | | } |
| | | }, |
| | |
| | | "当前周" |
| | | ] |
| | | }, |
| | | 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 |
| | | } |
| | | }, |
| | |
| | | "当前月" |
| | | ] |
| | | }, |
| | | 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; |
| | |
| | | </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> |
| | |
| | | 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 |
| | |
| | | > |
| | | </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" |
| | |
| | | </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 |
| | |
| | | yapi_base_url: "", |
| | | yapi_openapi_token: "", |
| | | jira_bearer_token: "", |
| | | jira_project_key: "" |
| | | jira_project_key: "", |
| | | groups: [] |
| | | }, |
| | | responsibleOptions: [], |
| | | groupOptions: [], |
| | | rules: { |
| | | name: [ |
| | | { |
| | |
| | | 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("此操作将永久删除该项目, 是否继续?", "提示", { |
| | |
| | | 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`; |
| | | } |
| | | }); |
| | | } |
| | |
| | | this.projectForm.yapi_openapi_token = ""; |
| | | this.projectForm.jira_bearer_token = ""; |
| | | this.projectForm.jira_project_key = ""; |
| | | this.projectForm.groups = []; |
| | | }, |
| | | closeEditDialog(formName) { |
| | | this.editVisible = false; |
| | |
| | | }); |
| | | } |
| | | }); |
| | | }, |
| | | 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; |
| | |
| | | <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> |
| | |
| | | <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"> |
| | |
| | | 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" |
| | |
| | | > |
| | | <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> |
| | |
| | | search: "", |
| | | searchDebounce: null, |
| | | selectReports: [], |
| | | selectedReports: {}, |
| | | currentRow: "", |
| | | currentPage: 1, |
| | | pageSize: 10, |
| | |
| | | }; |
| | | }, |
| | | 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; |
| | |
| | | this.reportStatus = ""; |
| | | this.currentPage = 1; |
| | | this.onlyMe = false; |
| | | this.selectedReports = {}; |
| | | this.getReportList(); |
| | | }, |
| | | handleCurrentChange() { |
| | |
| | | }) |
| | | .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 |
| | |
| | | }) |
| | | .then(resp => { |
| | | this.reportData = resp; |
| | | this.selectedReports = {}; |
| | | }); |
| | | }, |
| | | handleRunFailCase(row) { |
| | |
| | | .delAllReports({ data: this.selectReports }) |
| | | .then(resp => { |
| | | this.$message.success(resp.msg); |
| | | this.selectedReports = {}; |
| | | this.getReportList(); |
| | | }); |
| | | }); |
| | |
| | | .then(resp => { |
| | | this.reportData = resp; |
| | | this.loading = false; |
| | | this.selectedReports = {}; |
| | | }); |
| | | }, |
| | | handleShowReRun(row) { |
| | |
| | | this.currentPage = 1; |
| | | this.getReportList(); |
| | | }, 300); |
| | | }, |
| | | tableRowClassName({ row }) { |
| | | return row.success === false ? 'warning-row' : '' |
| | | } |
| | | }, |
| | | watch: { |
| | |
| | | .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> |
| | |
| | | .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) |