<template>
|
<el-container>
|
<el-header style="background: #F7F7F7; padding: 0; height: 50px">
|
<div>
|
<div
|
style="display: flex; padding-top: 10px; padding-left: 10px"
|
>
|
<el-button
|
type="primary"
|
size="small"
|
icon="el-icon-circle-plus"
|
:title="
|
isSuperuser ? '添加项目' : '权限不足, 请联系管理员'
|
"
|
:disabled="!isSuperuser"
|
@click="handleAdd"
|
>添加项目</el-button
|
>
|
<el-button
|
type="success"
|
size="small"
|
icon="el-icon-data-line"
|
@click="dashBoardVisible = true"
|
>项目看板</el-button
|
>
|
<el-button
|
type="info"
|
size="small"
|
icon="el-icon-arrow-left"
|
:disabled="loading || projectData.previous === null"
|
@click="getPagination(projectData.previous)"
|
>上一页</el-button
|
>
|
<el-button
|
type="info"
|
size="small"
|
:disabled="loading || projectData.next === null"
|
@click="getPagination(projectData.next)"
|
>下一页<i
|
class="el-icon-arrow-right el-icon--right"
|
></i>
|
</el-button>
|
|
<el-dialog
|
title="添加项目"
|
width="30%"
|
:close-on-click-modal="false"
|
:visible.sync="dialogVisible"
|
:before-close="handleBeforeClose"
|
:style="{ 'text-align': 'center' }"
|
>
|
<el-form
|
:model="projectForm"
|
:rules="rules"
|
ref="projectForm"
|
label-width="150px"
|
class="project"
|
>
|
<el-form-item label="项目名称" prop="name">
|
<el-input
|
v-model.trim="projectForm.name"
|
clearable
|
></el-input>
|
</el-form-item>
|
|
<el-form-item label="项目描述" prop="desc">
|
<el-input
|
v-model.trim="projectForm.desc"
|
clearable
|
></el-input>
|
</el-form-item>
|
|
<el-form-item label="负责人" prop="responsible">
|
<el-select
|
v-model="projectForm.responsible"
|
placeholder="请选择项目负责人"
|
filterable
|
clearable
|
:style="{ width: '100%' }"
|
>
|
<el-option
|
v-for="(item,
|
index) in responsibleOptions"
|
:key="index"
|
:label="item.label"
|
:value="item.value"
|
></el-option>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="YAPI地址" prop="yapi_base_url">
|
<el-input
|
v-model.trim="projectForm.yapi_base_url"
|
clearable
|
></el-input>
|
</el-form-item>
|
|
<el-form-item
|
label="YAPI token"
|
prop="yapi_openapi_token"
|
>
|
<el-input
|
v-model.trim="
|
projectForm.yapi_openapi_token
|
"
|
clearable
|
></el-input>
|
</el-form-item>
|
|
<!-- TODO: 后面需优化去掉,使用TAPD代替-->
|
<el-form-item
|
label="JIRA bearer token"
|
prop="jira_bearer_token"
|
>
|
<el-input
|
v-model.trim="projectForm.jira_bearer_token"
|
clearable
|
></el-input>
|
</el-form-item>
|
|
<el-form-item
|
label="JIRA project_key"
|
prop="jira_project_key"
|
>
|
<el-input
|
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>
|
|
<span
|
slot="footer"
|
class="dialog-footer"
|
style="display: flex; justify-content: flex-end"
|
>
|
<el-button @click="closeAddDialog('projectForm')"
|
>取 消</el-button
|
>
|
<el-button
|
type="primary"
|
@click="handleConfirm('projectForm')"
|
>确 定</el-button
|
>
|
</span>
|
</el-dialog>
|
|
<el-dialog
|
title="编辑项目"
|
width="30%"
|
:close-on-click-modal="false"
|
:visible.sync="editVisible"
|
:before-close="handleBeforeClose"
|
>
|
<el-form
|
:model="projectForm"
|
:rules="rules"
|
ref="projectForm"
|
label-width="150px"
|
>
|
<el-form-item label="项目名称" prop="name">
|
<el-input
|
v-model.trim="projectForm.name"
|
clearable
|
></el-input>
|
</el-form-item>
|
<el-form-item label="项目描述" prop="desc">
|
<el-input
|
v-model.trim="projectForm.desc"
|
clearable
|
></el-input>
|
</el-form-item>
|
<el-form-item label="负责人" prop="responsible">
|
<el-select
|
v-model="projectForm.responsible"
|
placeholder="请选择项目负责人"
|
filterable
|
clearable
|
:style="{ width: '100%' }"
|
:disabled="
|
userName === projectForm.responsible &&
|
!isSuperuser
|
"
|
>
|
<el-option
|
v-for="(item,
|
index) in responsibleOptions"
|
:key="index"
|
:label="item.label"
|
:value="item.value"
|
></el-option>
|
</el-select>
|
</el-form-item>
|
|
<el-form-item label="YAPI地址" prop="yapi_base_url">
|
<el-input
|
v-model.trim="projectForm.yapi_base_url"
|
clearable
|
>
|
</el-input>
|
</el-form-item>
|
|
<el-form-item
|
label="YAPI token"
|
prop="yapi_openapi_token"
|
>
|
<el-input
|
v-model.trim="
|
projectForm.yapi_openapi_token
|
"
|
clearable
|
>
|
</el-input>
|
</el-form-item>
|
|
<el-form-item
|
label="JIRA bearer token"
|
prop="jira_bearer_token"
|
>
|
<el-input
|
v-model.trim="projectForm.jira_bearer_token"
|
clearable
|
>
|
</el-input>
|
</el-form-item>
|
|
<el-form-item
|
label="JIRA project_key"
|
prop="jira_project_key"
|
>
|
<el-input
|
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>
|
<span
|
slot="footer"
|
class="dialog-footer"
|
style="display: flex; justify-content: flex-end"
|
>
|
<el-button @click="closeEditDialog('projectForm')"
|
>取 消</el-button
|
>
|
<el-button
|
type="primary"
|
@click="handleConfirm('projectForm')"
|
>确 定</el-button
|
>
|
</span>
|
</el-dialog>
|
</div>
|
</div>
|
</el-header>
|
|
<!-- 现代化警示牌区域 -->
|
<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="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>
|
|
<!-- 风险预警监控牌 -->
|
<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: '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>
|
</div>
|
</div>
|
|
<el-drawer
|
:destroy-on-close="true"
|
:with-header="false"
|
:modal="false"
|
size="90%"
|
:visible.sync="dashBoardVisible"
|
>
|
<ProjectDashBoard></ProjectDashBoard>
|
</el-drawer>
|
<el-container>
|
<el-main style="padding: 0; margin-left: 10px">
|
<el-table
|
v-loading="loading"
|
highlight-current-row
|
:data="projectData.results"
|
border
|
stripe
|
:show-header="projectData.results.length > 0"
|
style="width: 100%;"
|
:fit="true"
|
>
|
<el-table-column
|
label="项目名称"
|
width="250"
|
align="center"
|
show-overflow-tooltip
|
>
|
<template v-slot="scope">
|
<span
|
class="cell-ellipsis"
|
style="font-size: 18px; font-weight: bold; cursor:pointer;"
|
@click="handleCellClick(scope.row)"
|
>{{ scope.row.name }}</span
|
>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="负责人" width="100" align="center" show-overflow-tooltip>
|
<template v-slot="scope">
|
<span class="cell-ellipsis">{{
|
scope.row.responsible
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="定时任务数" width="120" align="center">
|
<template v-slot="scope">
|
<el-tag
|
:type="scope.row.task_count > 0 ? 'success' : 'info'"
|
effect="dark"
|
>
|
<i class="el-icon-timer"></i>
|
{{ scope.row.task_count || 0 }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="启动任务数" width="120" align="center">
|
<template v-slot="scope">
|
<div style="display: flex; align-items: center">
|
<el-progress
|
:percentage="scope.row.enabled_task_count / scope.row.task_count * 100 || 0"
|
:show-text="false"
|
:stroke-width="10"
|
:color="scope.row.enabled_task_count > 0 ? '#67C23A' : '#909399'"
|
style="width: 60px; margin-right: 5px"
|
/>
|
<el-tag
|
:type="scope.row.enabled_task_count > 0 ? 'success' : 'info'"
|
>
|
{{ scope.row.enabled_task_count || 0 }}
|
</el-tag>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="近24h失败报告" width="120" align="center">
|
<template v-slot="scope">
|
<el-tag type="danger" effect="dark">
|
{{ scope.row.recent_failed_count || 0 }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="接口总数" width="130" align="center">
|
<template v-slot="scope">
|
<div class="metric-card"
|
style="background: #fff9e6; cursor: pointer;"
|
@click="$router.push(`/lunarlink/api_record/${scope.row.id}`)">
|
<div class="metric-value" style="color: #d4a30e">{{ scope.row.api_total || 0 }}</div>
|
<div class="metric-label" style="color: #666">APIs</div>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="用例总数" width="130" align="center">
|
<template v-slot="scope">
|
<div class="metric-card"
|
style="background: #f5f9ff; cursor: pointer;"
|
@click="$router.push(`/lunarlink/test_case/${scope.row.id}/auto_test`)">
|
<div class="metric-value" style="color: #1a73e8">{{ scope.row.case_total || 0 }}</div>
|
<div class="metric-label" style="color: #666">Cases</div>
|
</div>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="项目描述"
|
width="250"
|
align="center"
|
show-overflow-tooltip
|
>
|
<template v-slot="scope">
|
<span class="cell-ellipsis">{{
|
scope.row.desc
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column
|
label="创建时间"
|
width="200"
|
align="center"
|
>
|
<template v-slot="scope">
|
<span>{{
|
scope.row.create_time | datetimeFormat
|
}}</span>
|
</template>
|
</el-table-column>
|
|
<el-table-column label="操作" align="center">
|
<template v-slot="scope">
|
<div
|
style="display: flex; justify-content: center;"
|
>
|
<el-button
|
size="medium"
|
@click="handleCellClick(scope.row)"
|
>进入
|
</el-button>
|
<el-button
|
size="medium"
|
type="primary"
|
:title="
|
isSuperuser ||
|
userName === scope.row.responsible
|
? '编辑项目'
|
: '权限不足,请联系管理员'
|
"
|
:disabled="
|
!(
|
isSuperuser ||
|
userName === scope.row.responsible
|
)
|
"
|
@click="handleEdit(scope.$index, scope.row)"
|
>编辑
|
</el-button>
|
<el-button
|
size="medium"
|
type="danger"
|
v-show="isSuperuser"
|
:title="
|
isSuperuser
|
? '删除项目'
|
: '权限不足, 请联系管理员'
|
"
|
@click="
|
handleDelete(scope.$index, scope.row)
|
"
|
>删除</el-button
|
>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-main>
|
</el-container>
|
</el-container>
|
</template>
|
|
<script>
|
import ProjectDashBoard from "@/pages/project/ProjectDashBoard.vue";
|
export default {
|
name: "ProjectList",
|
components: { ProjectDashBoard },
|
data() {
|
return {
|
recentEnabledProjects: [], // 存储有启动任务的项目
|
recentFailedProjects: [], // 存储有失败报告的项目
|
animationDuration: '20s', // 根据内容长度自动调整
|
isSuperuser: this.$store.state.is_superuser,
|
userName: this.$store.state.user,
|
task_count: 0,
|
dialogVisible: false,
|
dashBoardVisible: false,
|
editVisible: false,
|
hoveringSuccess: false,
|
hoveringError: false,
|
projectData: {
|
results: []
|
},
|
projectForm: {
|
name: "",
|
desc: "",
|
responsible: this.$store.state.name,
|
id: "",
|
yapi_base_url: "",
|
yapi_openapi_token: "",
|
jira_bearer_token: "",
|
jira_project_key: "",
|
groups: []
|
},
|
responsibleOptions: [],
|
groupOptions: [],
|
rules: {
|
name: [
|
{
|
required: true,
|
message: "请输入项目名称",
|
trigger: "blur"
|
},
|
{
|
min: 1,
|
max: 50,
|
message: "最多不超过50个字符",
|
trigger: "blur"
|
}
|
],
|
desc: [
|
{
|
required: true,
|
message: "简要描述下该项目",
|
trigger: "blur"
|
},
|
{
|
min: 1,
|
max: 100,
|
message: "最多不超过100个字符",
|
trigger: "blur"
|
}
|
],
|
responsible: [
|
{
|
required: true,
|
message: "请选择项目负责人",
|
trigger: "change"
|
}
|
],
|
yapi_base_url: [
|
{
|
required: false,
|
message: "YAPI openapi的url",
|
trigger: "blur"
|
}
|
],
|
yapi_openapi_token: [
|
{
|
required: false,
|
message: "YAPI openapi的token",
|
trigger: "blur"
|
}
|
],
|
jira_bearer_token: [
|
{
|
required: false,
|
message: "JIRA bearer_token",
|
trigger: "blur"
|
}
|
],
|
jira_project_key: [
|
{
|
required: false,
|
message: "jira_project_key",
|
trigger: "blur"
|
}
|
]
|
},
|
loading: true
|
};
|
},
|
methods: {
|
handleCellClick(row) {
|
this.$store.commit("setRouterName", "ProjectDetail");
|
this.$store.commit("setProjectName", row.name);
|
this.setLocalValue("routerName", "ProjectDetail");
|
// 在vuex严格模式下, commit会经过mutation函数不会报错, set直接修改会报错
|
this.setLocalValue("projectName", row.name);
|
this.$router.push({
|
name: "ProjectDetail",
|
params: { id: row["id"] }
|
});
|
},
|
formatDateTime(timestamp) {
|
const date = new Date(timestamp * 1000);
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
},
|
handleAdd() {
|
this.dialogVisible = true;
|
this.resetProjectForm();
|
},
|
handleEdit(index, row) {
|
this.editVisible = true;
|
this.projectForm.name = row["name"];
|
this.projectForm.desc = row["desc"];
|
this.projectForm.responsible = row["responsible"];
|
this.projectForm.id = row["id"];
|
this.projectForm.yapi_base_url = row["yapi_base_url"];
|
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("此操作将永久删除该项目, 是否继续?", "提示", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "waring"
|
}).then(() => {
|
this.$api
|
.deleteProject({ data: { id: row["id"] } })
|
.then(resp => {
|
if (resp["success"]) {
|
this.$message.success(resp.msg);
|
this.getProjectList();
|
} else {
|
this.$message.error(resp.msg);
|
}
|
});
|
});
|
},
|
resetForm(formName) {
|
this.$refs[formName].resetFields();
|
},
|
handleConfirm(formName) {
|
this.$refs[formName].validate(valid => {
|
if (valid) {
|
this.dialogVisible = false;
|
this.editVisible = false;
|
let obj;
|
|
if (this.projectForm.id === "") {
|
obj = this.$api.addProject(this.projectForm);
|
} else {
|
obj = this.$api.updateProject(this.projectForm);
|
}
|
|
obj.then(resp => {
|
if (resp["success"]) {
|
this.$message.success(resp.msg);
|
this.getProjectList();
|
} else {
|
this.$message.error(resp.msg);
|
}
|
this.resetProjectForm();
|
});
|
} else {
|
if (this.projectForm.id !== "") {
|
this.editVisible = true;
|
} else {
|
this.dialogVisible = true;
|
}
|
return false;
|
}
|
});
|
},
|
async getPagination(url) {
|
this.loading = true;
|
try {
|
const response = await this.$api.getPagination(url);
|
this.projectData = response;
|
|
// 获取每个项目的定时任务数
|
for (const project of this.projectData.results) {
|
try {
|
const taskResponse = await this.$api.getTasksCount(project.id);
|
project.task_count = taskResponse.count
|
} catch (error) {
|
console.error(`获取项目${project.id}的定时任务数失败:`, error);
|
project.task_count = 0;
|
}
|
}
|
} catch (error) {
|
console.error(error);
|
} finally {
|
this.loading = false;
|
}
|
},
|
async getProjectList() {
|
this.loading = true;
|
this.recentFailedProjects = [];
|
this.recentEnabledProjects = []; // 新增:重置启动任务列表
|
|
try {
|
const response = await this.$api.getProjectList();
|
this.projectData = response;
|
|
const tempFailedProjects = [];
|
const tempEnabledProjects = []; // 新增:临时存储启动任务项目
|
|
const projects = await Promise.all(
|
this.projectData.results.map(async (project) => {
|
try {
|
// 获取项目详情中的统计信息
|
const detailResponse = await this.$api.getProjectDetail(project.id);
|
project.api_total = (
|
(detailResponse.api_count_by_create_type &&
|
detailResponse.api_count_by_create_type.count)
|
? detailResponse.api_count_by_create_type.count.reduce((a, b) => a + b, 0)
|
: 0
|
);
|
|
project.case_total = (
|
(detailResponse.case_count_by_tag &&
|
detailResponse.case_count_by_tag.count)
|
? detailResponse.case_count_by_tag.count.reduce((a, b) => a + b, 0)
|
: 0
|
);
|
|
// 获取所有任务(不启用后端过滤)
|
const taskResponse = await this.$api.getTasksCount(project.id);
|
const enabledTasks = taskResponse.results.filter(task => task.enabled);
|
|
// 更新任务数统计
|
project.task_count = taskResponse.count;
|
project.enabled_task_count = enabledTasks.length;
|
|
// 收集启动任务项目
|
if (project.enabled_task_count > 0) {
|
tempEnabledProjects.push({
|
id: project.id,
|
name: project.name,
|
enabled_task_count: project.enabled_task_count
|
});
|
}
|
|
// 失败报告统计逻辑
|
const reportResponse = await this.$api.getReportsForProject(project.id);
|
const now = Math.floor(Date.now() / 1000);
|
const twentyFourHoursAgo = now - 24 * 3600;
|
|
const recentFailed = reportResponse.results.filter(report => {
|
return report.time &&
|
report.time.start_at &&
|
report.time.start_at >= twentyFourHoursAgo;
|
});
|
project.recent_failed_count = recentFailed.length || 0;
|
if (project.recent_failed_count > 0) {
|
tempFailedProjects.push({
|
report_time: recentFailed[0].time.start_at, // 使用实际失败报告的时间
|
id: project.id,
|
name: project.name,
|
recent_failed_count: project.recent_failed_count
|
});
|
}
|
|
} catch (error) {
|
console.error(`获取项目 ${project.id} 数据失败:`, error);
|
project.task_count = 0;
|
project.enabled_task_count = 0;
|
project.api_total = 0;
|
project.case_total = 0;
|
}
|
return project;
|
})
|
);
|
|
// 更新数据
|
this.recentEnabledProjects = [...tempEnabledProjects];
|
this.recentFailedProjects = [...tempFailedProjects];
|
this.projectData.results = projects;
|
|
} catch (error) {
|
this.$message.error("获取项目列表失败,超时请重新登录" );
|
} finally {
|
this.loading = false;
|
this.$nextTick(() => {
|
if (this.recentFailedProjects.length > 0 || this.recentEnabledProjects.length > 0) {
|
// 根据项目数量和内容长度计算动画持续时间
|
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 : 600;
|
this.animationDuration = `${Math.max(15, (contentWidth / viewportWidth) * 25)}s`;
|
}
|
});
|
}
|
},
|
resetProjectForm() {
|
this.projectForm.name = "";
|
this.projectForm.desc = "";
|
this.projectForm.responsible = this.$store.state.name;
|
this.projectForm.id = "";
|
this.projectForm.yapi_base_url = "";
|
this.projectForm.yapi_openapi_token = "";
|
this.projectForm.jira_bearer_token = "";
|
this.projectForm.jira_project_key = "";
|
this.projectForm.groups = [];
|
},
|
closeEditDialog(formName) {
|
this.editVisible = false;
|
this.resetForm(formName);
|
},
|
closeAddDialog(formName) {
|
this.dialogVisible = false;
|
this.resetForm(formName);
|
},
|
handleBeforeClose(done) {
|
this.resetForm("projectForm");
|
done();
|
},
|
getUserList() {
|
this.$api.getUserList().then(resp => {
|
for (let i = 0; i < resp.length; i++) {
|
this.responsibleOptions.push({
|
label: resp[i].name,
|
value: resp[i].name
|
});
|
}
|
});
|
},
|
getGroupList() {
|
this.$api.getGroupList().then(resp => {
|
this.groupOptions = resp;
|
});
|
}
|
},
|
created() {
|
this.getProjectList();
|
this.getUserList();
|
this.getGroupList();
|
}
|
};
|
</script>
|
|
<style scoped>
|
/* 现代化警示牌样式 */
|
.dashboard-alerts {
|
display: flex;
|
gap: 16px;
|
margin: 16px;
|
flex-wrap: wrap;
|
}
|
|
.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);
|
}
|
|
.alert-card:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
}
|
|
/* 成功状态卡片 */
|
.success-card {
|
border-left: 4px solid #52c41a;
|
}
|
|
.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;
|
}
|
|
/* 错误状态卡片 */
|
.error-card {
|
border-left: 4px solid #ff4d4f;
|
}
|
|
.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;
|
}
|
|
.scroll-content {
|
display: flex;
|
align-items: center;
|
height: 100%;
|
animation: modern-scroll linear infinite;
|
animation-play-state: running;
|
white-space: nowrap;
|
}
|
|
.alert-item {
|
display: flex;
|
align-items: center;
|
padding: 0 24px;
|
height: 100%;
|
cursor: pointer;
|
transition: background-color 0.2s;
|
}
|
|
.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-weight: 500;
|
color: #262626;
|
min-width: 120px;
|
}
|
|
.task-count {
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
}
|
|
.count-number {
|
font-size: 20px;
|
font-weight: 700;
|
line-height: 1;
|
}
|
|
.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;
|
text-align: center;
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
transition: transform 0.2s;
|
}
|
.metric-card:hover {
|
transform: translateY(-2px);
|
}
|
.metric-value {
|
font-size: 24px;
|
font-weight: 600;
|
color: #606266;
|
margin-bottom: 4px;
|
}
|
.metric-label {
|
font-size: 14px;
|
color: #909399;
|
letter-spacing: 0.5px;
|
}
|
</style>
|