update: redis配置适配密码格式

create:加入自动化测试管理模块
This commit is contained in:
zhangtao
2025-02-11 17:00:55 +08:00
parent 99207cac3b
commit 7e38dcaf4f
103 changed files with 9476 additions and 74 deletions
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getApiCaseList(query) {
return request({
url: "/api/v1/autotest/apicase/list",
method: "get",
params: query,
});
}
export function getApiCaseDetail(query) {
return request({
url: "/api/v1/autotest/apicase/detail",
method: "get",
params: query,
});
}
export function createApiCase(body) {
return request({
url: "/api/v1/autotest/apicase/create",
method: "post",
data: body,
});
}
export function updateApiCase(body) {
return request({
url: "/api/v1/autotest/apicase/update",
method: "put",
data: body,
});
}
export function deleteApiCase(query) {
return request({
url: "/api/v1/autotest/apicase/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getEnvironmentList(query) {
return request({
url: "/api/v1/autotest/environment/list",
method: "get",
params: query,
});
}
export function getEnvironmentDetail(query) {
return request({
url: "/api/v1/autotest/environment/detail",
method: "get",
params: query,
});
}
export function createEnvironment(body) {
return request({
url: "/api/v1/autotest/environment/create",
method: "post",
data: body,
});
}
export function updateEnvironment(body) {
return request({
url: "/api/v1/autotest/environment/update",
method: "put",
data: body,
});
}
export function deleteEnvironment(query) {
return request({
url: "/api/v1/autotest/environment/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getGlobaldataList(query) {
return request({
url: "/api/v1/autotest/globaldata/list",
method: "get",
params: query,
});
}
export function getGlobaldataDetail(query) {
return request({
url: "/api/v1/autotest/globaldata/detail",
method: "get",
params: query,
});
}
export function createGlobaldata(body) {
return request({
url: "/api/v1/autotest/globaldata/create",
method: "post",
data: body,
});
}
export function updateGlobaldata(body) {
return request({
url: "/api/v1/autotest/globaldata/update",
method: "put",
data: body,
});
}
export function deleteGlobaldata(query) {
return request({
url: "/api/v1/autotest/globaldata/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getModuleList(query) {
return request({
url: "/api/v1/autotest/module/list",
method: "get",
params: query,
});
}
export function getModuleDetail(query) {
return request({
url: "/api/v1/autotest/module/detail",
method: "get",
params: query,
});
}
export function createModule(body) {
return request({
url: "/api/v1/autotest/module/create",
method: "post",
data: body,
});
}
export function updateModule(body) {
return request({
url: "/api/v1/autotest/module/update",
method: "put",
data: body,
});
}
export function deleteModule(query) {
return request({
url: "/api/v1/autotest/module/delete",
method: "delete",
params: query,
});
}
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getNotificationConfigList(query) {
return request({
url: "/api/v1/autotest/notificationconfig/list",
method: "get",
params: query,
});
}
export function getNotificationConfigDetail(query) {
return request({
url: "/api/v1/autotest/notificationconfig/detail",
method: "get",
params: query,
});
}
export function createNotificationConfig(body) {
return request({
url: "/api/v1/autotest/notificationconfig/create",
method: "post",
data: body,
});
}
export function updateNotificationConfig(body) {
return request({
url: "/api/v1/autotest/notificationconfig/update",
method: "put",
data: body,
});
}
export function deleteNotificationConfig(query) {
return request({
url: "/api/v1/autotest/notificationconfig/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getProjectList(query) {
return request({
url: "/api/v1/autotest/project/list",
method: "get",
params: query,
});
}
export function getProjectDetail(query) {
return request({
url: "/api/v1/autotest/project/detail",
method: "get",
params: query,
});
}
export function createProject(body) {
return request({
url: "/api/v1/autotest/project/create",
method: "post",
data: body,
});
}
export function updateProject(body) {
return request({
url: "/api/v1/autotest/project/update",
method: "put",
data: body,
});
}
export function deleteProject(query) {
return request({
url: "/api/v1/autotest/project/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getReportList(query) {
return request({
url: "/api/v1/autotest/report/list",
method: "get",
params: query,
});
}
export function getReportDetail(query) {
return request({
url: "/api/v1/autotest/report/detail",
method: "get",
params: query,
});
}
export function createReport(body) {
return request({
url: "/api/v1/autotest/report/create",
method: "post",
data: body,
});
}
export function updateReport(body) {
return request({
url: "/api/v1/autotest/report/update",
method: "put",
data: body,
});
}
export function deleteReport(query) {
return request({
url: "/api/v1/autotest/report/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getSuiteList(query) {
return request({
url: "/api/v1/autotest/suite/list",
method: "get",
params: query,
});
}
export function getSuiteDetail(query) {
return request({
url: "/api/v1/autotest/suite/detail",
method: "get",
params: query,
});
}
export function createSuite(body) {
return request({
url: "/api/v1/autotest/suite/create",
method: "post",
data: body,
});
}
export function updateSuite(body) {
return request({
url: "/api/v1/autotest/suite/update",
method: "put",
data: body,
});
}
export function deleteSuite(query) {
return request({
url: "/api/v1/autotest/suite/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getTaskList(query) {
return request({
url: "/api/v1/autotest/task/list",
method: "get",
params: query,
});
}
export function getTaskDetail(query) {
return request({
url: "/api/v1/autotest/task/detail",
method: "get",
params: query,
});
}
export function createTask(body) {
return request({
url: "/api/v1/autotest/task/create",
method: "post",
data: body,
});
}
export function updateTask(body) {
return request({
url: "/api/v1/autotest/task/update",
method: "put",
data: body,
});
}
export function deleteTask(query) {
return request({
url: "/api/v1/autotest/task/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getUICaseList(query) {
return request({
url: "/api/v1/autotest/uicase/list",
method: "get",
params: query,
});
}
export function getUICaseDetail(query) {
return request({
url: "/api/v1/autotest/uicase/detail",
method: "get",
params: query,
});
}
export function createUICase(body) {
return request({
url: "/api/v1/autotest/uicase/create",
method: "post",
data: body,
});
}
export function updateUICase(body) {
return request({
url: "/api/v1/autotest/uicase/update",
method: "put",
data: body,
});
}
export function deleteUICase(query) {
return request({
url: "/api/v1/autotest/uicase/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getUIElementList(query) {
return request({
url: "/api/v1/autotest/uielement/list",
method: "get",
params: query,
});
}
export function getUIElementDetail(query) {
return request({
url: "/api/v1/autotest/uielement/detail",
method: "get",
params: query,
});
}
export function createUIElement(body) {
return request({
url: "/api/v1/autotest/uielement/create",
method: "post",
data: body,
});
}
export function updateUIElement(body) {
return request({
url: "/api/v1/autotest/uielement/update",
method: "put",
data: body,
});
}
export function deleteUIElement(query) {
return request({
url: "/api/v1/autotest/uielement/delete",
method: "delete",
params: query,
});
}
+42
View File
@@ -0,0 +1,42 @@
import request from "@/utils/request";
export function getUIPageList(query) {
return request({
url: "/api/v1/autotest/uipage/list",
method: "get",
params: query,
});
}
export function getUIPageDetail(query) {
return request({
url: "/api/v1/autotest/uipage/detail",
method: "get",
params: query,
});
}
export function createUIPage(body) {
return request({
url: "/api/v1/autotest/uipage/create",
method: "post",
data: body,
});
}
export function updateUIPage(body) {
return request({
url: "/api/v1/autotest/uipage/update",
method: "put",
data: body,
});
}
export function deleteUIPage(query) {
return request({
url: "/api/v1/autotest/uipage/delete",
method: "delete",
params: query,
});
}
@@ -0,0 +1,648 @@
<template>
<a-modal v-model:open="debugModalState.open"
title="接口调试"
:width="1200"
:footer="null"
:bodyStyle="{ padding: '16px', height: 'calc(100vh - 200px)', overflow: 'auto' }"
:maskClosable="false"
:keyboard="false"
@cancel="handleCancel"
>
<!-- 原有的主体布局代码保持不变 -->
<div class="debug-container">
<!-- 1. 优化请求配置区域 -->
<div class="request-section">
<a-form layout="vertical">
<!-- 优化URL输入区 -->
<a-row :gutter="16">
<a-col :span="3">
<a-form-item label="请求方法">
<a-select v-model:value="debugRequest.method"
:dropdownMatchSelectWidth="false"
@change="handleMethodChange">
<a-select-option v-for="method in HTTP_METHODS" :key="method">
<span :class="getMethodClass(method)">{{ method }}</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="17">
<a-form-item label="请求地址">
<a-input-group compact>
<a-select v-model:value="protocol" style="width: 100px">
<a-select-option value="http">http://</a-select-option>
<a-select-option value="https">https://</a-select-option>
</a-select>
<a-auto-complete
v-model:value="debugRequest.url"
:options="urlHistory"
style="width: calc(100% - 100px)"
placeholder="请输入完整的URL地址"
@select="handleUrlSelect"
/>
</a-input-group>
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item label=" ">
<a-space>
<a-tooltip title="Ctrl + Enter">
<a-button type="primary" @click="sendRequest" :loading="loading">
发送请求
<template #icon><SendOutlined /></template>
</a-button>
</a-tooltip>
<a-dropdown>
<a-button>
更多 <DownOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="save" @click="saveAsFavorite">
<StarOutlined /> 保存到收藏
</a-menu-item>
<a-menu-item key="copy" @click="copyRequest">
<CopyOutlined /> 复制请求
</a-menu-item>
<a-menu-item key="reset" @click="confirmReset">
<ReloadOutlined /> 重置
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</a-form-item>
</a-col>
</a-row>
<!-- 2. 优化请求参数配置区 -->
<a-tabs v-model:activeKey="activeTab" class="debug-tabs">
<!-- Headers面板增加智能提示 -->
<a-tab-pane key="headers" tab="Headers">
<div class="tab-toolbar">
<a-space>
<a-dropdown :trigger="['click']">
<a-button type="link">
常用Headers <DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="addCommonHeader">
<a-menu-item v-for="header in commonHeaders" :key="header.key">
{{ header.key }}: {{ header.value }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-button type="link" @click="clearHeaders">清空</a-button>
</a-space>
</div>
<div class="param-table">
<div v-for="(item, index) in headersList" :key="index" class="param-row">
<a-input v-model:value="item.key" placeholder="Key" class="param-key" />
<a-input v-model:value="item.value" placeholder="Value" class="param-value" />
<a-button type="text" @click="removeHeader(index)" danger>
<DeleteOutlined />
</a-button>
</div>
<a-button type="link" @click="addHeader" class="add-btn">
<PlusOutlined /> 添加 Header
</a-button>
</div>
</a-tab-pane>
<!-- Query Params -->
<a-tab-pane key="params" tab="Params">
<div class="tab-toolbar">
<a-button type="link" @click="validateParams">验证参数</a-button>
</div>
<div class="param-table">
<div v-for="(item, index) in paramsList" :key="index" class="param-row">
<a-input v-model:value="item.key" placeholder="Key" class="param-key" />
<a-input v-model:value="item.value" placeholder="Value" class="param-value" />
<a-button type="text" @click="removeParam(index)" danger>
<DeleteOutlined />
</a-button>
</div>
<a-button type="link" @click="addParam" class="add-btn">
<PlusOutlined /> 添加参数
</a-button>
</div>
</a-tab-pane>
<!-- 3. 优化Body展示 -->
<a-tab-pane key="body" tab="Body">
<div class="body-section">
<a-radio-group v-model:value="bodyType" class="body-type" button-style="solid">
<a-radio-button value="none">none</a-radio-button>
<a-radio-button value="form-data">form-data</a-radio-button>
<a-radio-button value="json">JSON</a-radio-button>
<a-radio-button value="raw">Raw</a-radio-button>
</a-radio-group>
<div v-if="bodyType === 'form-data'" class="param-table">
<div v-for="(item, index) in formDataList" :key="index" class="param-row">
<a-input v-model:value="item.key" placeholder="Key" class="param-key" />
<a-input v-model:value="item.value" placeholder="Value" class="param-value" />
<a-button type="text" @click="removeFormData(index)" danger>
<DeleteOutlined />
</a-button>
</div>
<a-button type="link" @click="addFormData" class="add-btn">
<PlusOutlined /> 添加字段
</a-button>
</div>
<div v-else-if="bodyType === 'json'" class="json-editor">
<MonacoEditor v-model:value="jsonBody" :options="editorOptions" language="json"
@change="formatJSON" />
</div>
<!-- 增加快捷操作栏 -->
<div v-if="bodyType === 'json'" class="json-toolbar">
<a-space>
<a-button type="link" size="small" @click="formatJSON">
<FormatPainterOutlined /> 格式化
</a-button>
<a-button type="link" size="small" @click="compressJSON">
<CompressOutlined /> 压缩
</a-button>
<a-button type="link" size="small" @click="validateJSON">
<CheckOutlined /> 验证
</a-button>
</a-space>
</div>
</div>
</a-tab-pane>
<!-- Cookies -->
<a-tab-pane key="cookies" tab="Cookies">
<div class="param-table">
<!-- ...类似headers的配置代码... -->
</div>
</a-tab-pane>
</a-tabs>
</a-form>
<!-- 发送按钮区增加快捷键提示 -->
<div class="action-bar">
<a-space>
<a-button type="primary" @click="sendRequest" :loading="loading">
发送请求
<template #icon>
<SendOutlined />
</template>
</a-button>
<a-button @click="resetForm">
重置
<template #icon>
<ReloadOutlined />
</template>
</a-button>
<span class="shortcut-tip">快捷键: Ctrl + Enter</span>
</a-space>
</div>
</div>
<!-- 4. 优化响应结果区域 -->
<a-divider />
<div v-if="response" class="response-section">
<!-- 优化响应状态展示 -->
<div class="response-header">
<div :class="['status', getStatusClass(response.status)]">
<a-badge :status="getStatusBadge(response.status)" />
Status: {{ response.status }} {{ response.statusText }}
</div>
<div class="info">
<a-space>
<span>Time: {{ response.time }}ms</span>
<a-divider type="vertical" />
<span>Size: {{ response.size }}</span>
</a-space>
</div>
</div>
<!-- 优化响应内容展示 -->
<a-tabs :animated="false">
<a-tab-pane key="response" tab="Response">
<div class="response-toolbar">
<a-space>
<a-tooltip title="复制到剪贴板">
<a-button type="link" @click="copyResponse">
<CopyOutlined /> 复制
</a-button>
</a-tooltip>
<a-button type="link" @click="downloadResponse">
<DownloadOutlined /> 下载
</a-button>
<a-button type="link" @click="searchResponse">
<SearchOutlined /> 搜索
</a-button>
<a-select v-model:value="responseView" style="width: 100px">
<a-select-option value="pretty">Pretty</a-select-option>
<a-select-option value="raw">Raw</a-select-option>
<a-select-option value="preview">Preview</a-select-option>
</a-select>
</a-space>
</div>
<MonacoEditor :value="formatResponse(response.data)" :options="viewerOptions" language="json"
class="response-viewer" />
</a-tab-pane>
<a-tab-pane key="headers" tab="Headers">
<a-descriptions bordered :column="1">
<a-descriptions-item v-for="(value, key) in response.headers" :key="key" :label="key">
{{ value }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="cookies" tab="Cookies">
<!-- ...Cookie展示代码... -->
</a-tab-pane>
</a-tabs>
</div>
<a-empty v-else description="等待发送请求..." />
</div>
<!-- 5. 添加辅助功能弹窗 -->
<a-drawer
v-model:visible="searchDrawer.visible"
title="搜索响应内容"
placement="right"
:width="300"
>
<a-form layout="vertical">
<a-form-item label="搜索内容">
<a-input v-model:value="searchDrawer.keyword" allow-clear @pressEnter="handleSearch" />
</a-form-item>
<a-form-item label="搜索选项">
<a-checkbox v-model:checked="searchDrawer.caseSensitive">区分大小写</a-checkbox>
<a-checkbox v-model:checked="searchDrawer.useRegex">使用正则</a-checkbox>
</a-form-item>
</a-form>
<template #footer>
<a-button type="primary" @click="handleSearch">搜索</a-button>
</template>
</a-drawer>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined, SendOutlined, ReloadOutlined, DownOutlined, StarOutlined, CopyOutlined, FormatPainterOutlined, CompressOutlined, CheckOutlined, DownloadOutlined, SearchOutlined } from '@ant-design/icons-vue';
import type { DebugModalState } from './types';
import axios from 'axios';
// 常量定义
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
// 状态定义
const debugModalState = reactive<DebugModalState>({
open: false,
loading: false
});
const loading = ref(false);
const activeTab = ref('headers');
const bodyType = ref('none');
const jsonBody = ref('');
const response = ref(null);
// Monaco编辑器配置
const editorOptions = {
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
fontSize: 14,
theme: 'vs-dark'
};
const viewerOptions = {
...editorOptions,
readOnly: true
};
// Headers管理
const headersList = ref([]);
const addHeader = () => headersList.value.push({ key: '', value: '' });
const removeHeader = (index) => headersList.value.splice(index, 1);
// Params管理
const paramsList = ref([]);
const addParam = () => paramsList.value.push({ key: '', value: '' });
const removeParam = (index) => paramsList.value.splice(index, 1);
// FormData管理
const formDataList = ref([]);
const addFormData = () => formDataList.value.push({ key: '', value: '' });
const removeFormData = (index) => formDataList.value.splice(index, 1);
// 请求数据管理
const debugRequest = reactive({
method: 'GET',
url: '',
headers: {},
params: {},
data: null
});
// JSON格式化
const formatJSON = (value) => {
try {
const formatted = JSON.stringify(JSON.parse(value), null, 2);
jsonBody.value = formatted;
} catch (e) {
// 保持原值
}
};
// 发送请求
const sendRequest = async () => {
if (!debugRequest.url) {
message.error('请输入请求地址');
return;
}
loading.value = true;
try {
// 构建请求配置
const config = {
url: debugRequest.url,
method: debugRequest.method,
headers: {},
params: {},
data: null
};
// 处理Headers
headersList.value.forEach(item => {
if (item.key && item.value) {
config.headers[item.key] = item.value;
}
});
// 处理Params
paramsList.value.forEach(item => {
if (item.key && item.value) {
config.params[item.key] = item.value;
}
});
// 处理Body
if (bodyType.value === 'json') {
try {
config.data = JSON.parse(jsonBody.value);
} catch (e) {
throw new Error('JSON格式错误');
}
} else if (bodyType.value === 'form-data') {
const formData = new FormData();
formDataList.value.forEach(item => {
if (item.key && item.value) {
formData.append(item.key, item.value);
}
});
config.data = formData;
}
// 发送请求并记录时间
const startTime = Date.now();
const res = await axios(config);
const endTime = Date.now();
// 计算响应大小
const size = JSON.stringify(res.data).length;
const sizeStr = size > 1024 ? `${(size / 1024).toFixed(2)} KB` : `${size} B`;
// 设置响应数据
response.value = {
status: res.status,
statusText: res.statusText,
headers: res.headers,
data: res.data,
time: endTime - startTime,
size: sizeStr
};
} catch (error) {
response.value = {
status: error.response?.status || 500,
statusText: error.message,
headers: error.response?.headers || {},
data: error.response?.data || error.message,
time: 0,
size: '0 B'
};
message.error('请求失败: ' + error.message);
} finally {
loading.value = false;
}
};
// 重置表单
const resetForm = () => {
debugRequest.method = 'GET';
debugRequest.url = '';
headersList.value = [];
paramsList.value = [];
formDataList.value = [];
jsonBody.value = '';
bodyType.value = 'none';
response.value = null;
};
// 格式化响应数据
const formatResponse = (data) => {
try {
return typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
} catch (e) {
return data;
}
};
// 快捷键处理
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
});
const handleKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'Enter') {
sendRequest();
}
};
// 新增方法
const handleCancel = () => {
Modal.confirm({
title: '确认关闭',
content: '关闭后将丢失未保存的修改,是否继续?',
onOk: () => {
debugModalState.open = false;
resetForm();
}
});
};
const addCommonHeaders = () => {
headersList.value.push(
{ key: 'Content-Type', value: 'application/json' },
{ key: 'Accept', value: '*/*' }
);
};
const clearHeaders = () => {
Modal.confirm({
title: '确认清空',
content: '是否清空所有Headers?',
onOk: () => {
headersList.value = [];
}
});
};
const validateParams = () => {
// ...参数验证逻辑
};
const validateJSON = () => {
try {
JSON.parse(jsonBody.value);
message.success('JSON格式正确');
} catch (e) {
message.error('JSON格式错误:' + e.message);
}
};
const copyResponse = () => {
// ...复制响应内容
};
const downloadResponse = () => {
// ...下载响应内容
};
const searchResponse = () => {
// ...搜索响应内容
};
const protocol = ref('http');
const urlHistory = ref<Array<{value: string, label: string}>>([]);
const responseView = ref('pretty');
const commonHeaders = [
{ key: 'Content-Type', value: 'application/json' },
{ key: 'Accept', value: '*/*' },
{ key: 'Authorization', value: 'Bearer ' }
// 更多常用headers...
];
// 搜索抽屉状态
const searchDrawer = reactive({
visible: false,
keyword: '',
caseSensitive: false,
useRegex: false
});
// 方法扩展
const getMethodClass = (method: string) => {
const classes = {
GET: 'method-get',
POST: 'method-post',
PUT: 'method-put',
DELETE: 'method-delete'
};
return classes[method] || '';
};
const getStatusClass = (status: number) => {
if (status < 300) return 'success';
if (status < 400) return 'warning';
return 'error';
};
const getStatusBadge = (status: number) => {
if (status < 300) return 'success';
if (status < 400) return 'warning';
return 'error';
};
const handleMethodChange = (method: string) => {
// 根据方法自动添加相关headers
if (method === 'POST' || method === 'PUT') {
addCommonHeader({ key: 'Content-Type' });
}
};
const handleUrlSelect = (url: string) => {
// 保存到历史记录
if (!urlHistory.value.find(item => item.value === url)) {
urlHistory.value.unshift({ value: url, label: url });
if (urlHistory.value.length > 10) {
urlHistory.value.pop();
}
}
};
const confirmReset = () => {
Modal.confirm({
title: '确认重置',
content: '是否清空所有已配置的请求参数?',
onOk: resetForm
});
};
defineExpose({
debugModalState,
debugRequest,
headersList,
paramsList,
formDataList,
jsonBody,
bodyType,
resetForm
});
</script>
<style lang="scss" scoped>
.debug-container {
// ... 原有样式 ...
// 新增样式
.method-get { color: #52c41a; }
.method-post { color: #1890ff; }
.method-put { color: #faad14; }
.method-delete { color: #ff4d4f; }
.json-toolbar {
position: absolute;
right: 8px;
top: 8px;
z-index: 1;
background: rgba(255,255,255,0.9);
padding: 4px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.response-header {
.status {
display: flex;
align-items: center;
gap: 8px;
&.warning { color: #faad14; }
&.error { color: #ff4d4f; }
}
}
// ...其他原有样式...
}
</style>
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,127 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
url?: string;
method?: string;
headers?: string;
data_type?: string;
data?: string;
expected_status_code?: number;
expected_msg?: string;
expected_response?: string;
assertions_status_code?: string;
assertions_msg?: string;
assertions_response?: string;
module_id?: moduleTableDataType['id'];
project_id? : projectSeletorType['id'];
environment_id?: environmentTableDataType['id'];
global_data_id?: globalDataTableDataType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
export interface projectSeletorType {
id?: number;
name?: string;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface moduleTableDataType {
id?: number;
name?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface environmentTableDataType {
id?: number;
name?: string;
base_url?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface globalDataTableDataType {
id?: number;
name?: string;
value?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
// 添加模块弹窗类型定义
export type ModuleModalType = 'create' | 'update' | 'view';
export interface ModuleModalState {
open: boolean;
title: string;
loading: boolean;
type: ModuleModalType;
id: number | null;
}
// 请求方法类型
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
// k-v 格式定义
export interface KeyValue {
key: string;
value: string;
}
// 参数类型定义
export type ParamsType = 'params' | 'data' | 'json' | 'file';
// 断言规则定义
export type AssertionRule = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'in' | 'not in';
// 调试窗口状态
export interface DebugModalState {
open: boolean;
loading: boolean;
}
// 调试请求参数
export interface DebugRequestType {
method: HttpMethod;
url: string;
headers: Record<string, string>;
params: Record<string, string>;
body: any;
}
// 调试响应结果
export interface DebugResponseType {
status: number;
statusText: string;
headers: Record<string, string>;
data: any;
time: number;
size: string;
}
@@ -0,0 +1,473 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 主体内容区域 -->
<div class="main-content">
<div class="environment-list">
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称"
allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range"
value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="环境列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<!-- 使用 a-row a-col 划分左右布局 -->
<a-row :gutter="16">
<!-- 左侧部门树 -->
<a-col :span="4">
<a-tree v-model:selectedKeys="selectedProject" v-if="treeData.length"
:tree-data="treeData"
@select="handleProjectSelect"
:showLine="true"
:defaultExpandAll="true"
/>
</a-col>
<!-- 右侧用户列表 -->
<a-col :span="20">
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1
}}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消"
@confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-col>
</a-row>
</a-card>
</div>
</div>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建环境' : (modalTitle === 'view' ? '查看环境' : '修改环境')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="环境名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="环境地址">{{ detailState.base_url }}</a-descriptions-item>
<a-descriptions-item label="绑定项目" :span="1">
{{ projectList.find(p => p.id === detailState.project_id)?.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description
}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="环境名称" :rules="[{ required: true, message: '请输入环境名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入环境名称" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="环境地址" :rules="[{ required: true, message: '请输入环境地址' }]">
<a-input v-model:value="createState.base_url" placeholder="请输入环境地址" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="createState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{
project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="环境名称" :rules="[{ required: true, message: '请输入环境名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入环境名称" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="环境地址" :rules="[{ required: true, message: '请输入环境地址' }]">
<a-input v-model:value="updateState.base_url" placeholder="请输入环境地址" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="updateState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{
project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList } from '@/api/autotest/project'
import { getEnvironmentList, getEnvironmentDetail, createEnvironment, updateEnvironment, deleteEnvironment } from '@/api/autotest/environment'
import type { searchDataType, tableDataType, projectSeletorType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '环境名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '环境地址',
dataIndex: 'base_url',
ellipsis: true,
},
{
title: '绑定项目',
dataIndex: 'project_id',
ellipsis: true,
customRender: ({ text }) => {
const project = projectList.value.find(p => p.id === text);
return project ? project.name : '-';
}
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
name: '',
base_url: '',
project_id: undefined,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
base_url: '',
project_id: undefined,
description: ''
})
const detailState = ref<tableDataType>({})
const projectList = ref<projectSeletorType[]>([]);
const handProjectList = () => {
tableLoading.value = true;
let params = {};
getProjectList(params).then(response => {
const result = response.data;
projectList.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
if (currentProjectId.value) {
params['project_id'] = currentProjectId.value;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getEnvironmentList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => {
loadingData();
handProjectList(); // 新增:获取项目列表
});
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteEnvironment({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createEnvironment(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateEnvironment(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 新增项目选择相关变量
const selectedProject = ref<string[]>(['all']);
const currentProjectId = ref<number | null>(null);
// 添加树结构数据处理
const treeData = computed(() => {
return [
{
title: '全部项目',
key: 'all',
children: projectList.value.map(project => ({
title: project.name,
key: String(project.id)
}))
}
];
});
// 修改项目选择处理逻辑
const handleProjectSelect = (selectedKeys: string[]) => {
if (selectedKeys.length > 0) {
const key = selectedKeys[0];
currentProjectId.value = key === 'all' ? null : Number(key);
selectedProject.value = [key];
pagination.current = 1;
loadingData();
}
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,28 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
base_url?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
export interface projectSeletorType {
id: number;
name: string;
description: string;
}
@@ -0,0 +1,456 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="全局变量列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<!-- 使用 a-row a-col 划分左右布局 -->
<a-row :gutter="16">
<!-- 左侧部门树 -->
<a-col :span="4">
<a-tree v-model:selectedKeys="selectedProject" v-if="treeData.length" :tree-data="treeData"
@select="handleProjectSelect" :showLine="true" :defaultExpandAll="true" />
</a-col>
<!-- 右侧用户列表 -->
<a-col :span="20">
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消"
@confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-col>
</a-row>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建全局变量' : (modalTitle === 'view' ? '查看全局变量' : '修改全局变量')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="变量名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="变量值">{{ detailState.value }}</a-descriptions-item>
<a-descriptions-item label="绑定项目">{{ projectList.find(p => p.id ===
detailState.project_id)?.name || '-' }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description
}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="变量名称" :rules="[{ required: true, message: '请输入变量名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入变量名称" allowClear></a-input>
</a-form-item>
<a-form-item name="value" label="变量值" :rules="[{ required: true, message: '请输入变量值' }]">
<a-input v-model:value="createState.value" placeholder="请输入变量值" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="createState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{
project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="变量名称" :rules="[{ required: true, message: '请输入变量名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入变量名称" allowClear></a-input>
</a-form-item>
<a-form-item name="value" label="变量值" :rules="[{ required: true, message: '请输入变量值' }]">
<a-input v-model:value="updateState.value" placeholder="请输入变量值" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="updateState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{
project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList } from '@/api/autotest/project'
import { getGlobaldataList, getGlobaldataDetail, createGlobaldata, updateGlobaldata, deleteGlobaldata } from '@/api/autotest/globaldata'
import type { searchDataType, tableDataType, projectSeletorType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '变量名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '变量值',
dataIndex: 'value',
ellipsis: true,
},
{
title: '绑定项目',
dataIndex: 'project_id',
ellipsis: true,
customRender: ({ text }) => {
const project = projectList.value.find(p => p.id === text);
return project ? project.name : '-';
}
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
name: '',
value: '',
project_id: undefined,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
value: '',
project_id: undefined,
description: ''
})
const detailState = ref<tableDataType>({})
const projectList = ref<projectSeletorType[]>([]);
const handProjectList = () => {
tableLoading.value = true;
let params = {};
getProjectList(params).then(response => {
const result = response.data;
projectList.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
if (currentProjectId.value) {
params['project_id'] = currentProjectId.value;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getGlobaldataList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => {
loadingData();
handProjectList(); // 新增:获取项目列表
});
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteGlobaldata({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createGlobaldata(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateGlobaldata(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 新增项目选择相关变量
const selectedProject = ref<string[]>(['all']);
const currentProjectId = ref<number | null>(null);
// 添加树结构数据处理
const treeData = computed(() => {
return [
{
title: '全部项目',
key: 'all',
children: projectList.value.map(project => ({
title: project.name,
key: String(project.id)
}))
}
];
});
// 修改项目选择处理逻辑
const handleProjectSelect = (selectedKeys: string[]) => {
if (selectedKeys.length > 0) {
const key = selectedKeys[0];
currentProjectId.value = key === 'all' ? null : Number(key);
selectedProject.value = [key];
pagination.current = 1;
loadingData();
}
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,28 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
value?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
export interface projectSeletorType {
id: number;
name: string;
description: string;
}
@@ -0,0 +1,384 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="模块列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建模块' : (modalTitle === 'view' ? '查看模块' : '修改模块')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="模块名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="绑定项目">{{ projectList.find(p => p.id === detailState.project_id)?.name || '-' }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="模块名称" :rules="[{ required: true, message: '请输入模块名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入模块名称" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="createState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{ project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="模块名称" :rules="[{ required: true, message: '请输入模块名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入模块名称" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="updateState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{ project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList } from '@/api/autotest/project'
import { getModuleList, getModuleDetail, createModule, updateModule, deleteModule } from '@/api/autotest/module'
import type { searchDataType, tableDataType, projectSeletorType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '模块名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '绑定项目',
dataIndex: 'project_id',
ellipsis: true,
customRender: ({ text }) => {
const project = projectList.value.find(p => p.id === text);
return project ? project.name : '-';
}
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
name: '',
project_id: undefined,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
project_id: undefined,
description: ''
})
const detailState = ref<tableDataType>({})
const projectList = ref<projectSeletorType[]>([]);
const handProjectList = () => {
tableLoading.value = true;
let params = {};
getProjectList(params).then(response => {
const result = response.data;
projectList.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getModuleList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => {
loadingData();
handProjectList(); // 新增:获取项目列表
});
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteModule({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createModule(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateModule(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,27 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
export interface projectSeletorType {
id: number;
name: string;
description: string;
}
@@ -0,0 +1,590 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="通知类型" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="通知配置列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<!-- 使用 a-row a-col 划分左右布局 -->
<a-row :gutter="16">
<!-- 左侧部门树 -->
<a-col :span="4">
<a-tree v-model:selectedKeys="selectedProject" v-if="treeData.length" :tree-data="treeData"
@select="handleProjectSelect" :showLine="true" :defaultExpandAll="true" />
</a-col>
<!-- 右侧用户列表 -->
<a-col :span="20">
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消"
@confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-col>
</a-row>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建通知' : (modalTitle === 'view' ? '查看通知' : '修改通知')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="2" :labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号" :span="1">
{{ (pagination.current - 1) * pagination.pageSize + detailState.index + 1 }}
</a-descriptions-item>
<a-descriptions-item label="通知类型" :span="1">
{{ detailState.name }}
</a-descriptions-item>
<a-descriptions-item label="通知配置" :span="2">
<div v-for="(value, key) in detailState.config" :key="key">
{{ key }}: {{ value }}
</div>
</a-descriptions-item>
<a-descriptions-item label="绑定项目" :span="1">
{{ projectList.find(p => p.id === detailState.project_id)?.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="创建人" :span="1">
{{ detailState.creator ? detailState.creator.name : '-' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间" :span="1">
{{ detailState.created_at }}
</a-descriptions-item>
<a-descriptions-item label="修改时间" :span="1">
{{ detailState.updated_at }}
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
{{ detailState.description }}
</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="type" label="通知类型" :rules="[{ required: true, message: '请输入通知类型' }]">
<a-input v-model:value="createState.name" placeholder="请输入通知类型" allowClear></a-input>
</a-form-item>
<a-form-item name="config" label="通知配置" :rules="[{ required: true, message: '请输入通知配置' }]">
<a-form-item-rest>
<a-button type="link" @click="addConfigField('create')"
style="padding-left: 0; margin-bottom: 10px;">
<PlusOutlined /> 添加配置项
</a-button>
<div v-for="item in createConfigItems" :key="item.id"
style="margin-bottom: 10px; display: flex; gap: 8px;">
<a-input v-model:value="item.key" placeholder="键名" style="width: 200px"
@change="(e) => updateConfigKey('create', item.id, e.target.value)" />
<a-input v-model:value="item.value" placeholder="键值" style="width: 200px" />
<a-button type="text" @click="removeConfigField('create', item.id)" danger>
<DeleteOutlined />
</a-button>
</div>
</a-form-item-rest>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="createState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{
project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="type" label="通知类型" :rules="[{ required: true, message: '请输入通知类型' }]">
<a-input v-model:value="updateState.name" placeholder="请输入通知类型" allowClear></a-input>
</a-form-item>
<a-form-item name="config" label="通知配置" :rules="[{ required: true, message: '请输入通知配置' }]">
<a-form-item-rest>
<a-button type="link" @click="addConfigField('update')"
style="padding-left: 0; margin-bottom: 10px;">
<PlusOutlined /> 添加配置项
</a-button>
<div v-for="item in updateConfigItems" :key="item.id"
style="margin-bottom: 10px; display: flex; gap: 8px;">
<a-input v-model:value="item.key" placeholder="键名" style="width: 200px"
@change="(e) => updateConfigKey('update', item.id, e.target.value)" />
<a-input v-model:value="item.value" placeholder="键值" style="width: 200px" />
<a-button type="text" @click="removeConfigField('update', item.id)" danger>
<DeleteOutlined />
</a-button>
</div>
</a-form-item-rest>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="updateState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{
project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList } from '@/api/autotest/project'
import { getNotificationConfigList, getNotificationConfigDetail, createNotificationConfig, updateNotificationConfig, deleteNotificationConfig } from '@/api/autotest/notificationconfig'
import type { searchDataType, tableDataType, projectSeletorType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '通知类型',
dataIndex: 'name',
ellipsis: true,
},
{
title: '通知配置',
dataIndex: 'config',
ellipsis: true,
customRender: ({ text }) => {
const config = text as { [key: string]: string };
return Object.entries(config).map(([key, value]) => `${key}: ${value}`).join(', ');
}
},
{
title: '绑定项目',
dataIndex: 'project_id',
ellipsis: true,
customRender: ({ text }) => {
const project = projectList.value.find(p => p.id === text);
return project ? project.name : '-';
}
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const initialState = {
name: '',
config: {},
configKeys: {},
project_id: undefined,
description: ''
};
const createState = reactive<tableDataType>({...initialState});
const updateState = reactive<tableDataType>({...initialState});
const detailState = reactive<tableDataType>({
...initialState,
created_at: '',
updated_at: '',
creator: {
id: undefined,
name: '',
username: ''
}
})
const projectList = ref<projectSeletorType[]>([]);
const handProjectList = () => {
tableLoading.value = true;
let params = {};
getProjectList(params).then(response => {
const result = response.data;
projectList.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
if (currentProjectId.value) {
params['project_id'] = currentProjectId.value;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getNotificationConfigList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => {
loadingData();
handProjectList(); // 新增:获取项目列表
});
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
const selected = dataSource.value[index];
// 确保所有必要的属性都被初始化
Object.assign(detailState, {
id: selected.id,
type: selected.name || '',
config: selected.config || {},
project_id: selected.project_id,
description: selected.description || '',
created_at: selected.created_at || '',
updated_at: selected.updated_at || '',
creator: selected.creator || { id: undefined, name: '', username: '' },
index: index
});
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
// 重置状态
Object.assign(updateState, {
id: selected.id,
type: selected.name || '',
config: {},
project_id: selected.project_id,
description: selected.description || ''
});
// 初始化配置项数组
updateConfigItems.value = Object.entries(selected.config || {}).map(([key, value]) => ({
id: `temp_${Date.now()}_${key}`,
key,
value
}));
} else if (modalType === 'create') {
createConfigItems.value = [];
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteNotificationConfig({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
return;
}
const isCreate = modalTitle.value === 'create';
const form = isCreate ? createForm.value : updateForm.value;
const state = isCreate ? createState : updateState;
const configHandler = handleConfig(isCreate ? 'create' : 'update');
form.validate().then(() => {
const submitData = cloneDeep(state);
submitData.config = configHandler.getConfigForSubmit();
delete submitData.configKeys;
// 移除空值
Object.keys(submitData).forEach(key => {
if (isEmpty(submitData[key])) {
delete submitData[key];
}
});
const apiCall = isCreate ? createNotificationConfig : updateNotificationConfig;
apiCall(submitData).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
if (isCreate) {
Object.assign(createState, initialState);
}
message.success(response.data.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error);
});
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error);
});
}
// 添加配置项
const addConfigField = (type: 'create' | 'update') => handleConfig(type).addField();
const removeConfigField = (type: 'create' | 'update', key: string) => handleConfig(type).removeField(key);
const updateConfigKey = (type: 'create' | 'update', oldKey: string, newKey: string) => handleConfig(type).updateKey(oldKey, newKey);
// 优化配置项处理逻辑
const handleConfig = (type: 'create' | 'update') => {
const state = type === 'create' ? createState : updateState;
const items = type === 'create' ? createConfigItems : updateConfigItems;
return {
addField: () => {
items.value.push({
id: `temp_${Date.now().toString()}`,
key: '',
value: ''
});
},
removeField: (id: string) => {
const index = items.value.findIndex(item => item.id === id);
if (index > -1) {
items.value.splice(index, 1);
}
},
updateKey: (id: string, newKey: string) => {
if (!newKey) return;
// 检查新键是否已存在
if (items.value.some(item => item.key === newKey && item.id !== id)) {
message.error('键名已存在');
return;
}
const item = items.value.find(item => item.id === id);
if (item) {
item.key = newKey;
}
},
getConfigForSubmit: () => {
const config: { [key: string]: string } = {};
items.value.forEach(item => {
if (item.key && item.value) {
config[item.key] = item.value;
}
});
return config;
}
};
};
// 添加辅助状态接口
interface ConfigItem {
id: string;
key: string;
value: string;
}
// 添加辅助状态
const createConfigItems = ref<ConfigItem[]>([]);
const updateConfigItems = ref<ConfigItem[]>([]);
// 新增项目选择相关变量
const selectedProject = ref<string[]>(['all']);
const currentProjectId = ref<number | null>(null);
// 添加树结构数据处理
const treeData = computed(() => {
return [
{
title: '全部项目',
key: 'all',
children: projectList.value.map(project => ({
title: project.name,
key: String(project.id)
}))
}
];
});
// 修改项目选择处理逻辑
const handleProjectSelect = (selectedKeys: string[]) => {
if (selectedKeys.length > 0) {
const key = selectedKeys[0];
currentProjectId.value = key === 'all' ? null : Number(key);
selectedProject.value = [key];
pagination.current = 1;
loadingData();
}
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,26 @@
export interface searchDataType {
name?: string;
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
config?: { [key: string]: string };
project_id?: number;
description?: string;
created_at?: string;
updated_at?: string;
creator?: {
id?: number;
name?: string;
username?: string;
};
}
export interface projectSeletorType {
id: number;
name: string;
description: string;
}
@@ -0,0 +1,358 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="项目列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建项目' : (modalTitle === 'view' ? '查看项目' : '修改项目')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="项目名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description
}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="标题" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList, createProject, updateProject, deleteProject } from '@/api/autotest/project'
import type { searchDataType, tableDataType } from './types'
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
name: '',
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
description: ''
})
const detailState = ref<tableDataType>({})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '项目名称',
dataIndex: 'name',
ellipsis: true,
// align: 'center'
},
{
title: '备注',
dataIndex: 'description',
// align: 'center',
ellipsis: true,
// width: 500
},
{
title: '创建日期',
dataIndex: 'created_at',
// align: 'center',
ellipsis: true,
// width: 120
},
{
title: '更新日期',
dataIndex: 'updated_at',
// align: 'center',
ellipsis: true,
// width: 120
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getProjectList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteProject({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createProject(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateProject(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,20 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
@@ -0,0 +1,388 @@
<template>
<div>
<!-- 报告头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="报告列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建测试报告' : (modalTitle === 'view' ? '查看测试报告' : '修改测试报告')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="报告名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="任务编号">{{ detailState.task_id }}</a-descriptions-item>
<a-descriptions-item label="报告摘要">{{ detailState.summary }}</a-descriptions-item>
<a-descriptions-item label="详细报告">{{ detailState.details }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="报告名称" :rules="[{ required: true, message: '请输入报告名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入报告名称" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="任务编号" :rules="[{ required: true, message: '请输入任务编号' }]">
<a-input v-model:value="createState.task_id" placeholder="请输入任务编号" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="报告摘要" :rules="[{ required: true, message: '请输入报告摘要' }]">
<a-input v-model:value="createState.summary" placeholder="请输入报告摘要" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="详细报告" :rules="[{ required: true, message: '请输入详细报告' }]">
<a-input v-model:value="createState.details" placeholder="请输入详细报告" allowClear></a-input>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="报告名称" :rules="[{ required: true, message: '请输入报告名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入报告名称" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="任务编号" :rules="[{ required: true, message: '请输入任务编号' }]">
<a-input v-model:value="updateState.task_id" placeholder="请输入任务编号" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="报告摘要" :rules="[{ required: true, message: '请输入报告摘要' }]">
<a-input v-model:value="updateState.summary" placeholder="请输入报告摘要" allowClear></a-input>
</a-form-item>
<a-form-item name="base_url" label="详细报告" :rules="[{ required: true, message: '请输入详细报告' }]">
<a-input v-model:value="updateState.details" placeholder="请输入详细报告" allowClear></a-input>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getReportList, getReportDetail, createReport, updateReport, deleteReport } from '@/api/autotest/report'
import type { searchDataType, tableDataType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '报告名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '任务编号',
dataIndex: 'task_id',
ellipsis: true,
},
{
title: '报告摘要',
dataIndex: 'summary',
ellipsis: true,
},
{
title: '详细报告',
dataIndex: 'details',
ellipsis: true,
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
name: '',
task_id: undefined,
summary: '',
details: '',
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
task_id: undefined,
summary: '',
details: '',
description: ''
})
const detailState = ref<tableDataType>({})
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getReportList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteReport({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createReport(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateReport(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,23 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
task_id?: number;
summary?: string;
details?: string;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
+386
View File
@@ -0,0 +1,386 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="套件列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建套件' : (modalTitle === 'view' ? '查看套件' : '修改套件')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="套件名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="绑定项目">{{ projectList.find(p => p.id === detailState.project_id)?.name || '-' }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="套件名称" :rules="[{ required: true, message: '请输入套件名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入套件名称" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="createState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{ project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="套件名称" :rules="[{ required: true, message: '请输入套件名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入套件名称" allowClear></a-input>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="updateState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{ project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList } from '@/api/autotest/project'
import { getSuiteList, getSuiteDetail, createSuite, updateSuite, deleteSuite } from '@/api/autotest/suite'
import type { searchDataType, tableDataType, projectSeletorType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '套件名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '绑定项目',
dataIndex: 'project_id',
ellipsis: true,
customRender: ({ text }) => {
const project = projectList.value.find(p => p.id === text);
return project ? project.name : '-';
}
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
name: '',
project_id: undefined,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
project_id: undefined,
description: ''
})
const detailState = ref<tableDataType>({})
const projectList = ref<projectSeletorType[]>([]);
const handProjectList = () => {
tableLoading.value = true;
let params = {};
getProjectList(params).then(response => {
const result = response.data;
projectList.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getSuiteList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => {
loadingData();
handProjectList(); // 新增:获取项目列表
});
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteSuite({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createSuite(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateSuite(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,27 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
export interface projectSeletorType {
id: number;
name: string;
description: string;
}
+469
View File
@@ -0,0 +1,469 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="类型" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入类型" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="任务列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建
</a-button>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 400 }" :pagination="pagination" :style="{ minHeight: '420px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建任务' : (modalTitle === 'view' ? '查看任务' : '修改任务')
}}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="测试类型">{{ detailState.test_type }}</a-descriptions-item>
<a-descriptions-item label="执行状态">{{ detailState.status }}</a-descriptions-item>
<a-descriptions-item label="执行日志">{{ detailState.logs }}</a-descriptions-item>
<a-descriptions-item label="截图路径">{{ detailState.screenshot_path }}</a-descriptions-item>
<a-descriptions-item label="实际响应">{{ detailState.actual_response }}</a-descriptions-item>
<a-descriptions-item label="绑定模块">{{ testCaseList.find(t => t.id === detailState.test_case_id)?.name || '-' }}</a-descriptions-item>
<a-descriptions-item label="绑定项目">{{ projectList.find(p => p.id === detailState.project_id)?.name || '-' }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="测试类型" :rules="[{ required: true, message: '请输入测试类型' }]">
<a-input v-model:value="createState.test_type" placeholder="请输入任务类型" allowClear></a-input>
</a-form-item>
<a-form-item name="url" label="执行状态" :rules="[{ required: true, message: '请输入执行状态' }]">
<a-input v-model:value="createState.status" placeholder="请输入任务地址" allowClear></a-input>
</a-form-item>
<a-form-item name="method" label="执行日志" :rules="[{ required: true, message: '请输入执行日志' }]">
<a-input v-model:value="createState.logs" placeholder="请输入请求方法" allowClear></a-input>
</a-form-item>
<a-form-item name="headers" label="截图路径" :rules="[{ required: true, message: '请输入截图路径' }]">
<a-input v-model:value="createState.screenshot_path" placeholder="请输入请求头" allowClear></a-input>
</a-form-item>
<a-form-item name="headers" label="实际响应" :rules="[{ required: true, message: '请输入实际响应' }]">
<a-input v-model:value="createState.actual_response" placeholder="请输入请求头" allowClear></a-input>
</a-form-item>
<a-form-item name="module_id" label="绑定用例" :rules="[{ required: true, message: '请选择绑定用例' }]">
<a-select v-model:value="createState.test_case_id" placeholder="请选择绑定模块">
<a-select-option v-for="testCase in testCaseList" :key="testCase.id" :value="testCase.id">{{ testCase.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="createState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{ project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="测试类型" :rules="[{ required: true, message: '请输入测试类型' }]">
<a-input v-model:value="updateState.test_type" placeholder="请输入任务类型" allowClear></a-input>
</a-form-item>
<a-form-item name="url" label="执行状态" :rules="[{ required: true, message: '请输入执行状态' }]">
<a-input v-model:value="updateState.status" placeholder="请输入任务地址" allowClear></a-input>
</a-form-item>
<a-form-item name="method" label="执行日志" :rules="[{ required: true, message: '请输入执行日志' }]">
<a-input v-model:value="updateState.logs" placeholder="请输入请求方法" allowClear></a-input>
</a-form-item>
<a-form-item name="headers" label="截图路径" :rules="[{ required: true, message: '请输入截图路径' }]">
<a-input v-model:value="updateState.screenshot_path" placeholder="请输入请求头" allowClear></a-input>
</a-form-item>
<a-form-item name="headers" label="实际响应" :rules="[{ required: true, message: '请输入实际响应' }]">
<a-input v-model:value="updateState.actual_response" placeholder="请输入请求头" allowClear></a-input>
</a-form-item>
<a-form-item name="module_id" label="绑定用例" :rules="[{ required: true, message: '请选择绑定用例' }]">
<a-select v-model:value="updateState.test_case_id" placeholder="请选择绑定模块">
<a-select-option v-for="testCase in testCaseList" :key="testCase.id" :value="testCase.id">{{ testCase.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="project_id" label="绑定项目" :rules="[{ required: true, message: '请选择绑定项目' }]">
<a-select v-model:value="updateState.project_id" placeholder="请选择绑定项目">
<a-select-option v-for="project in projectList" :key="project.id" :value="project.id">{{ project.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getProjectList } from '@/api/autotest/project'
import { getApiCaseList } from '@/api/autotest/apicase'
import { getTaskList, getTaskDetail, createTask, updateTask, deleteTask } from '@/api/autotest/task'
import type { searchDataType, tableDataType, projectSeletorType, testCaseSeletorType } from './types'
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
ellipsis: true,
width: 80
},
{
title: '测试类型',
dataIndex: 'test_type',
ellipsis: true,
},
{
title: '执行状态',
dataIndex: 'status',
ellipsis: true,
},
{
title: '执行日志',
dataIndex: 'logs',
ellipsis: true,
},
{
title: '截图路径',
dataIndex: 'screenshot_path',
ellipsis: true,
},
{
title: '实际响应',
dataIndex: 'actual_response',
ellipsis: true,
},
{
title: '绑定用例',
dataIndex: 'test_case_id',
ellipsis: true,
customRender: ({ text }) => {
const testCase = testCaseList.value.find(t => t.id === text);
return testCase ? testCase.name : '-';
}
},
{
title: '绑定项目',
dataIndex: 'project_id',
ellipsis: true,
customRender: ({ text }) => {
const project = projectList.value.find(p => p.id === text);
return project ? project.name : '-';
}
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
},
{
title: '创建日期',
dataIndex: 'created_at',
ellipsis: true,
},
{
title: '更新日期',
dataIndex: 'updated_at',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
ellipsis: true,
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `${range[0]}-${range[1]} 条 / 总共 ${total}`
})
const createState = reactive<tableDataType>({
test_type: '',
status: undefined,
logs: '',
screenshot_path: '',
actual_response: '',
test_case_id: undefined,
project_id: undefined,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
test_type: '',
status: undefined,
logs: '',
screenshot_path: '',
actual_response: '',
test_case_id: undefined,
project_id: undefined,
description: ''
})
const detailState = ref<tableDataType>({})
const projectList = ref<projectSeletorType[]>([]);
const testCaseList = ref<testCaseSeletorType[]>([]);
const handProjectList = () => {
tableLoading.value = true;
let params = {};
getProjectList(params).then(response => {
const result = response.data;
projectList.value = result.data.items;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getTaskList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteTask({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createTask(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateTask(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+38
View File
@@ -0,0 +1,38 @@
export interface searchDataType {
name?: string
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
test_type?: string;
status?: string;
logs?: string;
screenshot_path?: string;
actual_response?: string;
test_case_id?: testCaseSeletorType['id'];
project_id? : projectSeletorType['id'];
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
export interface projectSeletorType {
id?: number;
name?: string;
description?: string;
}
export interface testCaseSeletorType {
id?: number;
name?: string;
description?: string;
}