| | |
| | | <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> |
| | | |
| | | <el-switch |
| | | style="margin-left: 10px" |
| | | v-model="onlyMe" |
| | | active-color="#13ce66" |
| | | inactive-color="#ff4949" |
| | | active-text="只看自己" |
| | | ></el-switch> |
| | | <div class="report__header--item"> |
| | | <el-switch |
| | | v-model="onlyMe" |
| | | 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" |
| | | <div class="report__body__list" v-loading="loading"> |
| | | <div |
| | | 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)" |
| | | > |
| | | <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 |
| | | :title="scope.row.name" |
| | | style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" |
| | | > |
| | | {{ scope.row.name }} |
| | | <div class="item-left"> |
| | | <div class="item-status"> |
| | | <div class="status-dot" :class="item.success ? 'success' : 'failure'"></div> |
| | | </div> |
| | | <div class="item-info"> |
| | | <div class="item-type-badge"> |
| | | <i :class="getTypeIcon(item.type)"></i> |
| | | <span>{{ item.type }}</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <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-name" :title="item.name"> |
| | | {{ item.name }} |
| | | </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="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> |
| | | </template> |
| | | </el-table-column> |
| | | </div> |
| | | </div> |
| | | |
| | | <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> |
| | | <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> |
| | | </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> |
| | | <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 |
| | | :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"> |
| | | <div class="item-stats"> |
| | | <div class="stat-group"> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">总用例</span> |
| | | <span class="stat-value">{{ item.stat.case_count }}</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-label">总接口</span> |
| | | <span class="stat-value">{{ item.stat.testsRun }}</span> |
| | | </div> |
| | | <div class="stat-item success"> |
| | | <span class="stat-label">通过</span> |
| | | <span class="stat-value">{{ item.stat.successes }}</span> |
| | | </div> |
| | | <div class="stat-item failure"> |
| | | <span class="stat-label">失败</span> |
| | | <span class="stat-value">{{ item.stat.failures }}</span> |
| | | </div> |
| | | <div class="stat-item error"> |
| | | <span class="stat-label">异常</span> |
| | | <span class="stat-value">{{ item.stat.errors }}</span> |
| | | </div> |
| | | <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> |
| | | </div> |
| | | <div class="progress-bar-container"> |
| | | <div class="progress-bar"> |
| | | <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> |
| | | <el-button |
| | | type="success" |
| | | icon="el-icon-view" |
| | | circle |
| | | size="mini" |
| | | @click=" |
| | | handleWatchReports(scope.row.id) |
| | | " |
| | | ></el-button> |
| | | <el-button |
| | | type="danger" |
| | | icon="el-icon-delete" |
| | | title="删除" |
| | | circle |
| | | size="mini" |
| | | @click=" |
| | | handleDelReports(scope.row.id) |
| | | " |
| | | ></el-button> |
| | | </div> |
| | | </el-row> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <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-size="pageSize" |
| | | :pager-count="5" |
| | | :total="reportData.count" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | background |
| | | ></el-pagination> |
| | | class="progress-fill success" |
| | | :style="{ width: calculatePercentage(item.stat.successes, item.stat.testsRun) + '%' }" |
| | | ></div> |
| | | <div |
| | | 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" |
| | | size="small" |
| | | @click.stop="handleWatchReports(item.id)" |
| | | > |
| | | 查看 |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | icon="el-icon-delete" |
| | | size="small" |
| | | plain |
| | | @click.stop="handleDelReports(item.id)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </div> |
| | | </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, 50]" |
| | | :page-size="pageSize" |
| | | :pager-count="5" |
| | | :total="reportData.count" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | background |
| | | ></el-pagination> |
| | | </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> |