测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue
@@ -24,6 +24,13 @@
                        >项目看板</el-button
                    >
                    <el-button
                        type="warning"
                        size="small"
                        icon="el-icon-user"
                        @click="handleUserManagement"
                        >用户管理</el-button
                    >
                    <el-button
                        type="info"
                        size="small"
                        icon="el-icon-arrow-left"
@@ -126,6 +133,24 @@
                                    v-model.trim="projectForm.jira_project_key"
                                    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>
@@ -234,6 +259,24 @@
                                >
                                </el-input>
                            </el-form-item>
                            <el-form-item label="项目分组" prop="groups">
                                <el-select
                                    v-model="projectForm.groups"
                                    placeholder="请选择项目分组(可多选)"
                                    filterable
                                    clearable
                                    multiple
                                    :style="{ width: '100%' }"
                                >
                                    <el-option
                                        v-for="(item, index) in groupOptions"
                                        :key="index"
                                        :label="item.name"
                                        :value="item.id"
                                    ></el-option>
                                </el-select>
                            </el-form-item>
                        </el-form>
                        <span
                            slot="footer"
@@ -254,73 +297,80 @@
            </div>
        </el-header>
        <div style="display: flex; align-items: center; margin: 10px">
            <!-- 新增的启动任务提示牌 -->
            <el-alert
                v-if="recentEnabledProjects.length > 0"
                title="正在启动的定时任务数"
                type="success"
                :closable="false"
                style="flex: 1; margin-right: 10px; border: 1px solid #d9f7be; border-radius: 8px; background: linear-gradient(135deg, #f6ffed, #e6ffd3);">
                <div class="scroll-wrapper" style="min-width: 180%">
        <!-- 现代化警示牌区域 -->
        <div class="dashboard-alerts">
            <!-- 运行状态监控牌 -->
            <div v-if="recentEnabledProjects.length > 0" class="alert-card success-card">
                <div class="alert-header">
                    <div class="alert-icon">
                        <i class="el-icon-success"></i>
                    </div>
                    <div class="alert-title">
                        <span class="title-text">运行状态监控</span>
                        <span class="title-sub">正在执行的定时任务</span>
                    </div>
                    <div class="alert-badge">{{ recentEnabledProjects.length }}</div>
                </div>
                <div class="alert-content">
                    <div class="scroll-container">
                        <div class="scroll-content" :style="{ animationDuration: animationDuration, animationPlayState: hoveringSuccess ? 'paused' : 'running' }"
                        @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>
                        <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="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>
            </el-alert>
            </div>
            <!-- 原有失败巡检预警牌 -->
            <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' }"
                        @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>
                        <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="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>
            </el-alert>
            </div>
        </div>
        <el-drawer
@@ -331,6 +381,18 @@
            :visible.sync="dashBoardVisible"
        >
            <ProjectDashBoard></ProjectDashBoard>
        </el-drawer>
        <el-drawer
            title="用户管理"
            :visible.sync="userManagementVisible"
            direction="rtl"
            size="80%"
            :before-close="handleUserManagementClose"
            :modal="false"
            class="user-management-drawer-wrapper"
        >
            <user-management-drawer v-if="userManagementVisible"></user-management-drawer>
        </el-drawer>
        <el-container>
            <el-main style="padding: 0; margin-left: 10px">
@@ -507,9 +569,10 @@
<script>
import ProjectDashBoard from "@/pages/project/ProjectDashBoard.vue";
import UserManagementDrawer from "@/pages/user/UserManagementDrawer.vue";
export default {
    name: "ProjectList",
    components: { ProjectDashBoard },
    components: { ProjectDashBoard, UserManagementDrawer },
    data() {
        return {
            recentEnabledProjects: [], // 存储有启动任务的项目
@@ -520,6 +583,7 @@
            task_count: 0,
            dialogVisible: false,
            dashBoardVisible: false,
            userManagementVisible: false,
            editVisible: false,
            hoveringSuccess: false,
            hoveringError: false,
@@ -534,9 +598,11 @@
                yapi_base_url: "",
                yapi_openapi_token: "",
                jira_bearer_token: "",
                jira_project_key: ""
                jira_project_key: "",
                groups: []
            },
            responsibleOptions: [],
            groupOptions: [],
            rules: {
                name: [
                    {
@@ -630,6 +696,12 @@
            this.dialogVisible = true;
            this.resetProjectForm();
        },
        handleUserManagement() {
            this.userManagementVisible = true;
        },
        handleUserManagementClose() {
            this.userManagementVisible = false;
        },
        handleEdit(index, row) {
            this.editVisible = true;
            this.projectForm.name = row["name"];
@@ -640,6 +712,7 @@
            this.projectForm.yapi_openapi_token = row["yapi_openapi_token"];
            this.projectForm.jira_project_key = row["jira_project_key"];
            this.projectForm.jira_bearer_token = row["jira_bearer_token"];
            this.projectForm.groups = row["groups"] || [];
        },
        handleDelete(index, row) {
            this.$confirm("此操作将永久删除该项目, 是否继续?", "提示", {
@@ -806,13 +879,13 @@
                this.loading = false;
                this.$nextTick(() => {
                    if (this.recentFailedProjects.length > 0 || this.recentEnabledProjects.length > 0) {
                        const contentWidth = Math.max(
                            this.recentFailedProjects.length * 400,
                            this.recentEnabledProjects.length * 300
                        );
                        // 根据项目数量和内容长度计算动画持续时间
                        const enabledWidth = this.recentEnabledProjects.length * 280;
                        const failedWidth = this.recentFailedProjects.length * 350;
                        const contentWidth = Math.max(enabledWidth, failedWidth);
                        const container = document.querySelector('.scroll-container');
                        const viewportWidth = container ? container.offsetWidth : 1200;
                        this.animationDuration = `${Math.max(10, (contentWidth / viewportWidth) * 20)}s`;
                        const viewportWidth = container ? container.offsetWidth : 600;
                        this.animationDuration = `${Math.max(15, (contentWidth / viewportWidth) * 25)}s`;
                    }
                });
            }
@@ -826,6 +899,7 @@
            this.projectForm.yapi_openapi_token = "";
            this.projectForm.jira_bearer_token = "";
            this.projectForm.jira_project_key = "";
            this.projectForm.groups = [];
        },
        closeEditDialog(formName) {
            this.editVisible = false;
@@ -848,104 +922,271 @@
                    });
                }
            });
        },
        getGroupList() {
            this.$api.getGroupList().then(resp => {
                this.groupOptions = resp;
            });
        }
    },
    created() {
        this.getProjectList();
        this.getUserList();
        this.getGroupList();
    }
};
</script>
<style scoped>
/* 公共标题样式 */
:deep(.el-alert__title) {
    font-size: 16px;
    font-weight: 600;
    padding-left: 8px;
    border-left: 3px solid;
    line-height: 1.5;
/* 现代化警示牌样式 */
.dashboard-alerts {
    display: flex;
    gap: 16px;
    margin: 16px;
    flex-wrap: wrap;
}
/* 失败预警样式 */
:deep(.el-alert[type="error"]) .el-alert__title {
    color: #ff4d4f !important;
    border-left-color: #ff4d4f;
}
:deep(.el-alert[type="error"]) .scroll-item {
    background: rgba(255, 77, 79, 0.08);
}
:deep(.el-alert[type="error"]) .alert-text {
    color: #ff4d4f;
}
:deep(.el-alert[type="error"]) .separator i {
    color: #ffa39e !important;
.alert-card {
    flex: 1;
    min-width: 300px;
    background: #ffffff;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
    border: 1px solid #f0f0f0;
    overflow: hidden;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 成功预警样式 */
:deep(.el-alert[type="success"]) .el-alert__title {
    color: #52c41a !important;
    border-left-color: #52c41a;
.alert-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
:deep(.el-alert[type="success"]) .scroll-item {
    background: rgba(82, 196, 26, 0.08);
/* 成功状态卡片 */
.success-card {
    border-left: 4px solid #52c41a;
}
:deep(.el-alert[type="success"]) .alert-text {
.success-card .alert-header {
    background: linear-gradient(135deg, #f6ffed 0%, #e6ffd3 100%);
}
.success-card .alert-icon {
    background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
}
.success-card .alert-badge {
    background: #52c41a;
}
.success-card .count-number {
    color: #52c41a;
}
:deep(.el-alert[type="success"]) .separator i {
    color: #b7eb8f !important;
/* 错误状态卡片 */
.error-card {
    border-left: 4px solid #ff4d4f;
}
/* 公共滚动样式 */
.scroll-wrapper {
    display: inline-block;
    min-width: 100%;
    margin: 0 -20px;
    overflow: hidden;
    position: relative;
    padding: 8px 0;
.error-card .alert-header {
    background: linear-gradient(135deg, #fff6f6 0%, #ffecec 100%);
}
.error-card .alert-icon {
    background: linear-gradient(135deg, #ff4d4f 0%, #ff7a7a 100%);
}
.error-card .alert-badge {
    background: #ff4d4f;
}
.error-card .failure-count {
    color: #ff4d4f;
}
/* 卡片头部样式 */
.alert-header {
    display: flex;
    align-items: center;
    padding: 16px 20px;
    border-bottom: 1px solid #f5f5f5;
}
.alert-icon {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.alert-icon i {
    font-size: 20px;
    color: white;
}
.alert-title {
    flex: 1;
    display: flex;
    flex-direction: column;
}
.title-text {
    font-size: 16px;
    font-weight: 600;
    color: #262626;
    line-height: 1.4;
}
.title-sub {
    font-size: 12px;
    color: #8c8c8c;
    margin-top: 2px;
}
.alert-badge {
    background: #1890ff;
    color: white;
    padding: 4px 8px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
    min-width: 24px;
    text-align: center;
}
/* 内容区域样式 */
.alert-content {
    padding: 0;
    height: 80px;
    overflow: hidden;
}
.scroll-container {
    height: 100%;
    overflow: hidden;
    position: relative;
    width: 100%;
}
.scroll-content {
    display: inline-block;
    white-space: nowrap;
    animation: project-list-scroll linear infinite;
    display: flex;
    align-items: center;
    height: 100%;
    animation: modern-scroll linear infinite;
    animation-play-state: running;
    white-space: nowrap;
}
.scroll-item {
    display: inline-flex;
.alert-item {
    display: flex;
    align-items: center;
    padding: 6px 12px;
    border-radius: 20px;
    margin: 0 10px;
    transition: all 0.3s;
    vertical-align: middle;
    padding: 0 24px;
    height: 100%;
    cursor: pointer;
    transition: background-color 0.2s;
}
.alert-text {
.alert-item:hover {
    background: rgba(0, 0, 0, 0.02);
}
.item-content {
    display: flex;
    align-items: center;
    gap: 16px;
}
.project-name {
    font-size: 14px;
    font-family: 'Microsoft YaHei', sans-serif;
    font-weight: 500;
    color: #262626;
    min-width: 120px;
}
.highlight {
    font-weight: 700;
    text-shadow: 0 1px 1px rgba(0,0,0,0.1);
}
.separator {
    display: inline-flex;
.task-count {
    display: flex;
    align-items: center;
    gap: 4px;
}
@keyframes project-list-scroll {
    0% { transform: translateX(180%); }
    100% { transform: translateX(-100%); }
.count-number {
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
}
.scroll-item:hover {
    transform: translateY(-2px);
    box-shadow: 0 3px 6px rgba(0,0,0,0.15);
.count-label {
    font-size: 12px;
    color: #8c8c8c;
}
.failure-info {
    display: flex;
    align-items: center;
    gap: 8px;
}
.failure-count {
    font-size: 20px;
    font-weight: 700;
    line-height: 1;
}
.failure-label {
    font-size: 12px;
    color: #8c8c8c;
}
.failure-time {
    font-size: 11px;
    color: #bfbfbf;
    background: #f5f5f5;
    padding: 2px 6px;
    border-radius: 4px;
}
.item-divider {
    width: 1px;
    height: 24px;
    background: #f0f0f0;
    margin: 0 0 0 24px;
}
/* 滚动动画 */
@keyframes modern-scroll {
    0% {
        transform: translateX(100%);
    }
    100% {
        transform: translateX(-100%);
    }
}
/* 响应式设计 */
@media (max-width: 768px) {
    .dashboard-alerts {
        flex-direction: column;
    }
    .alert-card {
        min-width: auto;
    }
    .item-content {
        flex-direction: column;
        gap: 8px;
        text-align: center;
    }
    .project-name {
        min-width: auto;
    }
}
/* 表格相关样式保持不变 */
.metric-card {
    padding: 12px;
    border-radius: 8px;