mirror of
https://gitee.com/mao-peng/MangoTestingPlatform.git
synced 2025-12-06 11:59:15 +08:00
优化部分页面的样式
This commit is contained in:
@@ -34,112 +34,121 @@ class IndexViews(ViewSet):
|
||||
try:
|
||||
api_result = TestSuiteDetails.objects.raw(
|
||||
"""
|
||||
SELECT
|
||||
weeks.id,
|
||||
weeks.yearweek,
|
||||
COALESCE(api_counts.total_count, 0) AS total_count
|
||||
FROM (
|
||||
SELECT 'id'as id,YEARWEEK(DATE_SUB(NOW(), INTERVAL n WEEK)) AS yearweek
|
||||
FROM (
|
||||
SELECT 0 AS n UNION ALL
|
||||
SELECT 1 UNION ALL
|
||||
SELECT 2 UNION ALL
|
||||
SELECT 3 UNION ALL
|
||||
SELECT 4 UNION ALL
|
||||
SELECT 5 UNION ALL
|
||||
SELECT 6 UNION ALL
|
||||
SELECT 7 UNION ALL
|
||||
SELECT 8 UNION ALL
|
||||
SELECT 9 UNION ALL
|
||||
SELECT 10 UNION ALL
|
||||
SELECT 11
|
||||
) weeks
|
||||
) weeks
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
MAX(test_suite_details.id) as id,
|
||||
YEARWEEK(create_time) AS yearweek,
|
||||
COUNT(YEARWEEK(create_time)) AS total_count
|
||||
FROM test_suite_details
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
|
||||
AND type = 1
|
||||
GROUP BY YEARWEEK(create_time)
|
||||
) api_counts ON weeks.yearweek = api_counts.yearweek
|
||||
ORDER BY weeks.yearweek;
|
||||
SELECT weeks.id,
|
||||
weeks.yearweek,
|
||||
COALESCE(api_counts.total_count, 0) AS total_count
|
||||
FROM (SELECT 'id' as id, YEARWEEK(DATE_SUB(NOW(), INTERVAL n WEEK)) AS yearweek
|
||||
FROM (SELECT 0 AS n
|
||||
UNION ALL
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT 2
|
||||
UNION ALL
|
||||
SELECT 3
|
||||
UNION ALL
|
||||
SELECT 4
|
||||
UNION ALL
|
||||
SELECT 5
|
||||
UNION ALL
|
||||
SELECT 6
|
||||
UNION ALL
|
||||
SELECT 7
|
||||
UNION ALL
|
||||
SELECT 8
|
||||
UNION ALL
|
||||
SELECT 9
|
||||
UNION ALL
|
||||
SELECT 10
|
||||
UNION ALL
|
||||
SELECT 11) weeks) weeks
|
||||
LEFT JOIN (SELECT MAX(test_suite_details.id) as id,
|
||||
YEARWEEK(create_time) AS yearweek,
|
||||
COUNT(YEARWEEK(create_time)) AS total_count
|
||||
FROM test_suite_details
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
|
||||
AND type = 1
|
||||
GROUP BY YEARWEEK(create_time)) api_counts ON weeks.yearweek = api_counts.yearweek
|
||||
ORDER BY weeks.yearweek;
|
||||
"""
|
||||
)
|
||||
ui_result = TestSuiteDetails.objects.raw(
|
||||
"""
|
||||
SELECT
|
||||
weeks.id,
|
||||
weeks.yearweek,
|
||||
COALESCE(api_counts.total_count, 0) AS total_count
|
||||
FROM (
|
||||
SELECT 'id'as id,YEARWEEK(DATE_SUB(NOW(), INTERVAL n WEEK)) AS yearweek
|
||||
FROM (
|
||||
SELECT 0 AS n UNION ALL
|
||||
SELECT 1 UNION ALL
|
||||
SELECT 2 UNION ALL
|
||||
SELECT 3 UNION ALL
|
||||
SELECT 4 UNION ALL
|
||||
SELECT 5 UNION ALL
|
||||
SELECT 6 UNION ALL
|
||||
SELECT 7 UNION ALL
|
||||
SELECT 8 UNION ALL
|
||||
SELECT 9 UNION ALL
|
||||
SELECT 10 UNION ALL
|
||||
SELECT 11
|
||||
) weeks
|
||||
) weeks
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
MAX(test_suite_details.id) as id,
|
||||
YEARWEEK(create_time) AS yearweek,
|
||||
COUNT(YEARWEEK(create_time)) AS total_count
|
||||
FROM test_suite_details
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
|
||||
AND type = 0
|
||||
GROUP BY YEARWEEK(create_time)
|
||||
) api_counts ON weeks.yearweek = api_counts.yearweek
|
||||
ORDER BY weeks.yearweek;
|
||||
|
||||
SELECT weeks.id,
|
||||
weeks.yearweek,
|
||||
COALESCE(api_counts.total_count, 0) AS total_count
|
||||
FROM (SELECT 'id' as id, YEARWEEK(DATE_SUB(NOW(), INTERVAL n WEEK)) AS yearweek
|
||||
FROM (SELECT 0 AS n
|
||||
UNION ALL
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT 2
|
||||
UNION ALL
|
||||
SELECT 3
|
||||
UNION ALL
|
||||
SELECT 4
|
||||
UNION ALL
|
||||
SELECT 5
|
||||
UNION ALL
|
||||
SELECT 6
|
||||
UNION ALL
|
||||
SELECT 7
|
||||
UNION ALL
|
||||
SELECT 8
|
||||
UNION ALL
|
||||
SELECT 9
|
||||
UNION ALL
|
||||
SELECT 10
|
||||
UNION ALL
|
||||
SELECT 11) weeks) weeks
|
||||
LEFT JOIN (SELECT MAX(test_suite_details.id) as id,
|
||||
YEARWEEK(create_time) AS yearweek,
|
||||
COUNT(YEARWEEK(create_time)) AS total_count
|
||||
FROM test_suite_details
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
|
||||
AND type = 0
|
||||
GROUP BY YEARWEEK(create_time)) api_counts ON weeks.yearweek = api_counts.yearweek
|
||||
ORDER BY weeks.yearweek;
|
||||
|
||||
"""
|
||||
)
|
||||
pytest_result = TestSuiteDetails.objects.raw(
|
||||
"""
|
||||
SELECT
|
||||
weeks.id,
|
||||
weeks.yearweek,
|
||||
COALESCE(api_counts.total_count, 0) AS total_count
|
||||
FROM (
|
||||
SELECT 'id'as id,YEARWEEK(DATE_SUB(NOW(), INTERVAL n WEEK)) AS yearweek
|
||||
FROM (
|
||||
SELECT 0 AS n UNION ALL
|
||||
SELECT 1 UNION ALL
|
||||
SELECT 2 UNION ALL
|
||||
SELECT 3 UNION ALL
|
||||
SELECT 4 UNION ALL
|
||||
SELECT 5 UNION ALL
|
||||
SELECT 6 UNION ALL
|
||||
SELECT 7 UNION ALL
|
||||
SELECT 8 UNION ALL
|
||||
SELECT 9 UNION ALL
|
||||
SELECT 10 UNION ALL
|
||||
SELECT 11
|
||||
) weeks
|
||||
) weeks
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
MAX(test_suite_details.id) as id,
|
||||
YEARWEEK(create_time) AS yearweek,
|
||||
COUNT(YEARWEEK(create_time)) AS total_count
|
||||
FROM test_suite_details
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
|
||||
AND type = 2
|
||||
GROUP BY YEARWEEK(create_time)
|
||||
) api_counts ON weeks.yearweek = api_counts.yearweek
|
||||
ORDER BY weeks.yearweek;
|
||||
|
||||
SELECT weeks.id,
|
||||
weeks.yearweek,
|
||||
COALESCE(api_counts.total_count, 0) AS total_count
|
||||
FROM (SELECT 'id' as id, YEARWEEK(DATE_SUB(NOW(), INTERVAL n WEEK)) AS yearweek
|
||||
FROM (SELECT 0 AS n
|
||||
UNION ALL
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT 2
|
||||
UNION ALL
|
||||
SELECT 3
|
||||
UNION ALL
|
||||
SELECT 4
|
||||
UNION ALL
|
||||
SELECT 5
|
||||
UNION ALL
|
||||
SELECT 6
|
||||
UNION ALL
|
||||
SELECT 7
|
||||
UNION ALL
|
||||
SELECT 8
|
||||
UNION ALL
|
||||
SELECT 9
|
||||
UNION ALL
|
||||
SELECT 10
|
||||
UNION ALL
|
||||
SELECT 11) weeks) weeks
|
||||
LEFT JOIN (SELECT MAX(test_suite_details.id) as id,
|
||||
YEARWEEK(create_time) AS yearweek,
|
||||
COUNT(YEARWEEK(create_time)) AS total_count
|
||||
FROM test_suite_details
|
||||
WHERE create_time >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
|
||||
AND type = 2
|
||||
GROUP BY YEARWEEK(create_time)) api_counts ON weeks.yearweek = api_counts.yearweek
|
||||
ORDER BY weeks.yearweek;
|
||||
|
||||
"""
|
||||
)
|
||||
result_dict = {
|
||||
@@ -209,7 +218,7 @@ class IndexViews(ViewSet):
|
||||
"""
|
||||
active_user_counts = UserLogs.objects.values('user') \
|
||||
.annotate(total_logins=Count('id')) \
|
||||
.order_by('-total_logins')[:10]
|
||||
.order_by('-total_logins')[:30]
|
||||
name_list = []
|
||||
total_logins_list = []
|
||||
for user_count in active_user_counts:
|
||||
|
||||
176
mango-console/src/components/AssertionResult.vue
Normal file
176
mango-console/src/components/AssertionResult.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!assertionData || assertionData.length === 0" class="empty-placeholder">
|
||||
暂无断言结果
|
||||
</div>
|
||||
<div v-else class="assertion-list">
|
||||
<div
|
||||
v-for="(item, index) in assertionData"
|
||||
:key="index"
|
||||
class="assertion-item"
|
||||
:class="{ 'assertion-pass': isPass(item), 'assertion-fail': !isPass(item) }"
|
||||
>
|
||||
<div class="assertion-header">
|
||||
<span class="assertion-method">{{ item.method }}</span>
|
||||
<span
|
||||
class="assertion-status"
|
||||
:class="{ 'status-pass': isPass(item), 'status-fail': !isPass(item) }"
|
||||
>
|
||||
{{ isPass(item) ? '通过' : '失败' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="assertion-details">
|
||||
<div class="assertion-row">
|
||||
<span class="label">实际值:</span>
|
||||
<span class="value">{{ item.actual }}</span>
|
||||
</div>
|
||||
<div class="assertion-row">
|
||||
<span class="label">预期值:</span>
|
||||
<span class="value">{{ item.expect }}</span>
|
||||
</div>
|
||||
<div class="assertion-message">
|
||||
{{ item.ass_msg }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, computed } from 'vue'
|
||||
|
||||
interface AssertionItem {
|
||||
actual: string
|
||||
expect: string
|
||||
method: string
|
||||
ass_msg: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: AssertionItem[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const assertionData = computed(() => props.data || [])
|
||||
|
||||
// 判断断言是否通过
|
||||
const isPass = (item: AssertionItem) => {
|
||||
// 如果有明确的实际值和预期值,直接比较
|
||||
if (item.actual !== undefined && item.expect !== undefined) {
|
||||
// 检查ass_msg中是否包含失败或错误关键字
|
||||
if (item.ass_msg && (item.ass_msg.includes('失败') || item.ass_msg.includes('错误'))) {
|
||||
return false
|
||||
}
|
||||
return item.actual === item.expect
|
||||
}
|
||||
|
||||
// 如果没有明确的值比较,检查ass_msg
|
||||
if (item.ass_msg) {
|
||||
return !(item.ass_msg.includes('失败') || item.ass_msg.includes('错误'))
|
||||
}
|
||||
|
||||
// 默认返回true
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-placeholder {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.assertion-item {
|
||||
border-left: 4px solid #e0e0e0;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 0 4px 4px 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.assertion-item:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.assertion-pass {
|
||||
border-left-color: #52c41a;
|
||||
}
|
||||
|
||||
.assertion-fail {
|
||||
border-left-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.assertion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.assertion-method {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assertion-status {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pass {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
.status-fail {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border: 1px solid #ffccc7;
|
||||
}
|
||||
|
||||
.assertion-details {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assertion-row {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.assertion-message {
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 3px;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,20 +5,34 @@
|
||||
<!-- 支持多个字段的展示 -->
|
||||
<div v-for="(field, fieldIndex) in fieldConfig" :key="fieldIndex" class="key-value-field">
|
||||
<span class="field-label">{{ field.label }}:</span>
|
||||
<!-- 支持空字段名的情况 -->
|
||||
<a-input
|
||||
v-if="field.field"
|
||||
<!-- 支持级联选择器 -->
|
||||
<a-cascader
|
||||
v-if="field.type === 'cascader'"
|
||||
v-model="item[field.field]"
|
||||
:options="field.options || []"
|
||||
:placeholder="field.placeholder || `请选择${field.label}`"
|
||||
:expand-trigger="field.expandTrigger || 'hover'"
|
||||
:value-key="field.valueKey || 'key'"
|
||||
@change="(value) => field.onChange && field.onChange(value, item, index)"
|
||||
:class="field.className || 'key-cascader'"
|
||||
/>
|
||||
<!-- 支持文本域 -->
|
||||
<a-textarea
|
||||
v-else-if="field.field"
|
||||
v-model="item[field.field]"
|
||||
:placeholder="field.placeholder || `请输入${field.label}`"
|
||||
:auto-size="field.autoSize || { minRows: 1, maxRows: 3 }"
|
||||
@blur="onBlur"
|
||||
:class="field.className || 'key-input'"
|
||||
/>
|
||||
<!-- 支持数组直接绑定的情况 -->
|
||||
<a-input
|
||||
<a-textarea
|
||||
v-else
|
||||
v-model="item[field.field]"
|
||||
:model-value="item"
|
||||
:placeholder="field.placeholder || `请输入${field.label}`"
|
||||
@blur="onBlur"
|
||||
:auto-size="field.autoSize || { minRows: 1, maxRows: 3 }"
|
||||
@blur="(event) => onItemBlur(event, index)"
|
||||
@update:model-value="(value) => onItemUpdate(value, index, item)"
|
||||
:class="field.className || 'key-input'"
|
||||
/>
|
||||
</div>
|
||||
@@ -42,13 +56,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
// 定义字段配置接口
|
||||
interface FieldConfig {
|
||||
field?: string // 字段名(可选,为空时表示直接绑定数组元素)
|
||||
label: string // 显示标签
|
||||
className?: string // 自定义类名
|
||||
placeholder?: string // 占位符文本
|
||||
type?: 'input' | 'textarea' | 'cascader' // 字段类型
|
||||
// 级联选择器特有属性
|
||||
options?: any[] // 选项数据
|
||||
expandTrigger?: 'click' | 'hover' // 次级菜单展开方式
|
||||
valueKey?: string // 选项值的键名
|
||||
onChange?: (value: any, item: any, index: number) => void // 值改变时的回调
|
||||
autoSize?: { minRows: number; maxRows: number } // 文本域自动调整大小
|
||||
}
|
||||
|
||||
// 定义组件属性
|
||||
@@ -60,6 +80,11 @@
|
||||
onSave?: () => void // 保存回调(可选)
|
||||
}
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:item', index: number, value: any, item: any): void
|
||||
}>()
|
||||
|
||||
// 定义默认属性
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
emptyText: '暂无数据,点击上方"增加"按钮添加',
|
||||
@@ -76,6 +101,19 @@
|
||||
props.onSave()
|
||||
}
|
||||
}
|
||||
|
||||
// 数组元素更新时的处理
|
||||
const onItemUpdate = (value: any, index: number, item: any) => {
|
||||
// 通过事件通知父组件更新数组元素
|
||||
emit('update:item', index, value, item)
|
||||
}
|
||||
|
||||
// 数组元素失去焦点时的处理
|
||||
const onItemBlur = (event: any, index: number) => {
|
||||
if (props.onSave) {
|
||||
props.onSave()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -94,8 +132,9 @@
|
||||
|
||||
.key-value-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.key-value-field {
|
||||
@@ -103,6 +142,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
@@ -113,7 +153,8 @@
|
||||
|
||||
.key-input,
|
||||
.value-input,
|
||||
.sql-input {
|
||||
.sql-input,
|
||||
.key-cascader {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -131,4 +172,4 @@
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -111,6 +111,7 @@ export const useTableIndexColumn = function () {
|
||||
key: 'index',
|
||||
width: 100,
|
||||
dataIndex: 'index',
|
||||
align: 'center',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,7 @@
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" @cancel="handleCancel" @ok="handleOk">
|
||||
<template #title> 扫描二维码加群</template>
|
||||
<span>进群需要git点Starred(点星)</span>
|
||||
<a-space>
|
||||
<img alt="作者微信" src="/static/images/author.jpg" />
|
||||
<img alt="交流群" src="/static/images/group.jpg" />
|
||||
</a-space>
|
||||
</a-modal>
|
||||
</template>
|
||||
<ContactAuthor v-model:visible="visible" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -44,6 +35,9 @@
|
||||
import { webSocketURL } from '@/api/axios.config'
|
||||
import { SERVER } from '@/setting'
|
||||
import { useNotificationMessage } from '@/store/modules/notification-message'
|
||||
|
||||
// 导入联系作者组件
|
||||
import ContactAuthor from '@/views/index/components/ContactAuthor.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const options = [
|
||||
@@ -190,14 +184,7 @@
|
||||
connectWebSocket()
|
||||
}
|
||||
})
|
||||
const visible = ref(import.meta.env.VITE_IS_INDEX_WINDOW == 'true')
|
||||
|
||||
const handleOk = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const visible = ref(false)
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
@@ -56,6 +56,17 @@
|
||||
(index) => removeFrontSql1(pageData.record.front_custom, index)
|
||||
"
|
||||
:on-save="upDataCase"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
pageData.record.front_custom,
|
||||
index,
|
||||
value,
|
||||
pageData.record,
|
||||
'front_custom',
|
||||
upDataCase
|
||||
)
|
||||
"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="12" title="sql参数">
|
||||
@@ -71,6 +82,17 @@
|
||||
]"
|
||||
:on-delete-item="(index) => removeFrontSql1(pageData.record.front_sql, index)"
|
||||
:on-save="upDataCase"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
pageData.record.front_sql,
|
||||
index,
|
||||
value,
|
||||
pageData.record,
|
||||
'front_sql',
|
||||
upDataCase
|
||||
)
|
||||
"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="13" title="默认请求头">
|
||||
@@ -172,6 +194,17 @@
|
||||
"
|
||||
:on-save="upDataCase"
|
||||
empty-text='暂无sql参数语句,点击上方"增加"按钮添加'
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
pageData.record.posterior_sql,
|
||||
index,
|
||||
value,
|
||||
pageData.record,
|
||||
'posterior_sql',
|
||||
upDataCase
|
||||
)
|
||||
"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -319,6 +352,17 @@
|
||||
removeFrontSql(item.front_sql, index, 'front_sql', item.id)
|
||||
"
|
||||
:on-save="() => blurSave('front_sql', item.front_sql, item.id)"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
item.front_sql,
|
||||
index,
|
||||
value,
|
||||
item,
|
||||
'front_sql',
|
||||
() => blurSave('front_sql', item.front_sql, item.id)
|
||||
)
|
||||
"
|
||||
empty-text='暂无前置sql语句,点击上方"增加"按钮添加'
|
||||
/>
|
||||
</div>
|
||||
@@ -429,7 +473,17 @@
|
||||
{
|
||||
field: 'method',
|
||||
label: '断言方法',
|
||||
type: 'cascader',
|
||||
options: data.textAss,
|
||||
placeholder: '请选择断言方法',
|
||||
expandTrigger: 'hover',
|
||||
valueKey: 'key',
|
||||
onChange: (value, currentItem, currentIndex) =>
|
||||
handleJsonpathMethodChange(
|
||||
value,
|
||||
currentItem,
|
||||
currentIndex
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'expect',
|
||||
@@ -449,6 +503,17 @@
|
||||
:on-save="
|
||||
() => blurSave('ass_jsonpath', item.ass_jsonpath, item.id)
|
||||
"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
item.ass_jsonpath,
|
||||
index,
|
||||
value,
|
||||
item,
|
||||
'ass_jsonpath',
|
||||
() => blurSave('ass_jsonpath', item.ass_jsonpath, item.id)
|
||||
)
|
||||
"
|
||||
empty-text='暂无jsonpath断言,点击上方"增加"按钮添加'
|
||||
/>
|
||||
</div>
|
||||
@@ -466,47 +531,74 @@
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="32" title="通用断言">
|
||||
<div class="m-2">
|
||||
<a-space direction="vertical">
|
||||
<a-space v-for="(value, index) of item.ass_general" :key="index">
|
||||
<a-cascader
|
||||
v-model="item.ass_general[index].method"
|
||||
:default-value="item.ass_general[index].method"
|
||||
:options="data.ass"
|
||||
expand-trigger="hover"
|
||||
placeholder="请选择断言方法"
|
||||
value-key="key"
|
||||
@change="changeGeneralAss(value, index, item)"
|
||||
/>
|
||||
<a-space
|
||||
v-if="value?.value && value?.value?.parameter"
|
||||
direction="vertical"
|
||||
<KeyValueList
|
||||
:data-list="item.ass_general"
|
||||
:field-config="[
|
||||
{
|
||||
field: 'method',
|
||||
label: '断言方法',
|
||||
type: 'cascader',
|
||||
options: data.ass,
|
||||
placeholder: '请选择断言方法',
|
||||
expandTrigger: 'hover',
|
||||
valueKey: 'key',
|
||||
onChange: (value, currentItem, currentIndex) =>
|
||||
handleGeneralMethodChange(
|
||||
value,
|
||||
currentItem,
|
||||
currentIndex,
|
||||
item
|
||||
),
|
||||
},
|
||||
]"
|
||||
:on-delete-item="
|
||||
(index) =>
|
||||
removeFrontSql(
|
||||
item.ass_general,
|
||||
index,
|
||||
'ass_general',
|
||||
item.id
|
||||
)
|
||||
"
|
||||
:on-save="
|
||||
() => blurSave('ass_general', item.ass_general, item.id)
|
||||
"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
item.ass_general,
|
||||
index,
|
||||
value,
|
||||
item,
|
||||
'ass_general',
|
||||
() => blurSave('ass_general', item.ass_general, item.id)
|
||||
)
|
||||
"
|
||||
empty-text='暂无通用断言,点击上方"增加"按钮添加'
|
||||
>
|
||||
<template #extra="{ index, item: assItem }">
|
||||
<div
|
||||
v-if="assItem?.value && assItem?.value?.parameter"
|
||||
class="assertion-parameters-inline"
|
||||
>
|
||||
<a-textarea
|
||||
v-for="(param, pIdx) in value.value.parameter"
|
||||
<div
|
||||
v-for="(param, pIdx) in assItem.value.parameter"
|
||||
:key="param.f"
|
||||
v-model="value.value.parameter[pIdx].v"
|
||||
:placeholder="param.p"
|
||||
:required="param.d"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
style="width: 330px"
|
||||
@blur="blurSave('ass_general', item.ass_general, item.id)"
|
||||
/>
|
||||
</a-space>
|
||||
<a-button
|
||||
status="danger"
|
||||
type="text"
|
||||
@click="
|
||||
removeFrontSql(
|
||||
item.ass_general,
|
||||
index,
|
||||
'ass_general',
|
||||
item.id
|
||||
)
|
||||
"
|
||||
>移除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
class="parameter-item-inline"
|
||||
>
|
||||
<span class="parameter-label-inline">{{ param.n }}:</span>
|
||||
<a-textarea
|
||||
v-model="assItem.value.parameter[pIdx].v"
|
||||
:placeholder="param.p"
|
||||
:required="param.d"
|
||||
:auto-size="{ minRows: 1, maxRows: 2 }"
|
||||
class="parameter-input-inline"
|
||||
@blur="blurSave('ass_general', item.ass_general, item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</KeyValueList>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -549,6 +641,22 @@
|
||||
item.id
|
||||
)
|
||||
"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
item.posterior_response,
|
||||
index,
|
||||
value,
|
||||
item,
|
||||
'posterior_response',
|
||||
() =>
|
||||
blurSave(
|
||||
'posterior_response',
|
||||
item.posterior_response,
|
||||
item.id
|
||||
)
|
||||
)
|
||||
"
|
||||
empty-text='暂无响应结果提取,点击上方"增加"按钮添加'
|
||||
>
|
||||
<template #extra="{ index }">
|
||||
@@ -556,6 +664,7 @@
|
||||
size="small"
|
||||
status="success"
|
||||
@click="jsonpathTest(item, index)"
|
||||
style="margin-top: 18px;"
|
||||
>
|
||||
测试
|
||||
</a-button>
|
||||
@@ -591,6 +700,17 @@
|
||||
:on-save="
|
||||
() => blurSave('posterior_sql', item.posterior_sql, item.id)
|
||||
"
|
||||
@update:item="
|
||||
(index, value) =>
|
||||
updateArrayItem(
|
||||
item.posterior_sql,
|
||||
index,
|
||||
value,
|
||||
item,
|
||||
'posterior_sql',
|
||||
() => blurSave('posterior_sql', item.posterior_sql, item.id)
|
||||
)
|
||||
"
|
||||
empty-text='暂无后置sql处理语句,点击上方"增加"按钮添加'
|
||||
/>
|
||||
</div>
|
||||
@@ -631,7 +751,7 @@
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="6" title="断言结果">
|
||||
<div class="m-2">
|
||||
<JsonDisplay :data="item.result_data?.ass" />
|
||||
<AssertionResult :data="item.result_data?.ass" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
@@ -736,6 +856,8 @@
|
||||
} from '@/api/apitest/case-detailed-parameter'
|
||||
import { getSystemCacheDataKeyValue } from '@/api/system/cache_data'
|
||||
import KeyValueList from '@/components/KeyValueList.vue' // 引入新组件
|
||||
import AssertionResult from '@/components/AssertionResult.vue' // 引入断言结果组件
|
||||
// import CacheDataDisplay from '@/components/CacheDataDisplay.vue' // 引入缓存数据展示组件
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -1050,6 +1172,24 @@
|
||||
.catch(console.log)
|
||||
}
|
||||
|
||||
// 通用断言测试方法
|
||||
/*
|
||||
function testGeneralAssertion(item: any, index: number) {
|
||||
// 这里可以添加通用断言的测试逻辑
|
||||
Message.info('通用断言测试功能待实现')
|
||||
}
|
||||
|
||||
// 通用断言保存方法
|
||||
function saveGeneralAssertion(item: any, index: number) {
|
||||
blurSave('ass_general', item.ass_general, item.id)
|
||||
}
|
||||
|
||||
// 通用断言删除方法
|
||||
function removeGeneralAssertion(item: any, index: number) {
|
||||
removeFrontSql(item.ass_general, index, 'ass_general', item.id)
|
||||
}
|
||||
*/
|
||||
|
||||
function getCacheDataKeyValue() {
|
||||
getSystemCacheDataKeyValue('ass_select_value')
|
||||
.then((res) => {
|
||||
@@ -1284,6 +1424,52 @@
|
||||
getCacheDataKeyValue()
|
||||
})
|
||||
})
|
||||
|
||||
function updateArrayItem(
|
||||
array: any[],
|
||||
index: number,
|
||||
value: any,
|
||||
item: any,
|
||||
fieldName: string,
|
||||
saveCallback?: () => void
|
||||
) {
|
||||
// 更新数组元素
|
||||
if (array && index < array.length) {
|
||||
array[index] = value
|
||||
}
|
||||
// 如果提供了保存回调,则执行保存
|
||||
if (saveCallback) {
|
||||
saveCallback()
|
||||
}
|
||||
}
|
||||
|
||||
function handleJsonpathMethodChange(value: any, item: any, index: number) {
|
||||
// 保存更改
|
||||
blurSave('ass_jsonpath', item.ass_jsonpath, item.id)
|
||||
}
|
||||
|
||||
function handleGeneralMethodChange(
|
||||
value: any,
|
||||
currentItem: any,
|
||||
currentIndex: number,
|
||||
item: any
|
||||
) {
|
||||
const inputItem = findItemByValue(data.ass, value)
|
||||
if (inputItem && Array.isArray(inputItem.parameter)) {
|
||||
inputItem.parameter.forEach((param) => {
|
||||
if (typeof param.v === 'object' && param.v !== null) {
|
||||
try {
|
||||
param.v = JSON.stringify(param.v)
|
||||
} catch {
|
||||
param.v = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
item.ass_general[currentIndex].value = inputItem
|
||||
// 保存更改
|
||||
blurSave('ass_general', item.ass_general, item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1320,4 +1506,75 @@
|
||||
gap: 12px; /* 控制标签间距 */
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 通用断言样式 */
|
||||
.assertion-parameters {
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.parameter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.parameter-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.parameter-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 通用断言行内样式 */
|
||||
.assertion-parameters-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.parameter-item-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.parameter-label-inline {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.parameter-input-inline {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 确保KeyValueList中的所有元素都在一行 */
|
||||
:deep(.key-value-row) {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:deep(.key-value-field) {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
:deep(.assertion-parameters-inline) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
144
mango-console/src/views/index/components/AutomationStats.vue
Normal file
144
mango-console/src/views/index/components/AutomationStats.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div style="margin-top: 10px; max-height: 300px; overflow-y: auto;" class="stats-container">
|
||||
<!-- UI自动化统计 -->
|
||||
<div class="stats-category">
|
||||
<div class="category-title">UI自动化</div>
|
||||
<a-grid :cols="4" :colGap="8" :rowGap="8">
|
||||
<a-grid-item>
|
||||
<a-statistic title="元素个数" :value="uiStats.elementCount || 0" :value-style="{ color: '#165DFF' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="页面个数" :value="uiStats.pageCount || 0" :value-style="{ color: '#F77234' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="步骤个数" :value="uiStats.stepCount || 0" :value-style="{ color: '#00B42A' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="用例个数" :value="uiStats.caseCount || 0" :value-style="{ color: '#722ED1' }" />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
|
||||
<!-- API自动化统计 -->
|
||||
<div class="stats-category" style="margin-top: 10px;">
|
||||
<div class="category-title">API自动化</div>
|
||||
<a-grid :cols="3" :colGap="8" :rowGap="8">
|
||||
<a-grid-item>
|
||||
<a-statistic title="接口个数" :value="apiStats.interfaceCount || 0" :value-style="{ color: '#165DFF' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="用例个数" :value="apiStats.caseCount || 0" :value-style="{ color: '#F77234' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="Headers个数" :value="apiStats.headersCount || 0" :value-style="{ color: '#00B42A' }" />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
|
||||
<!-- Pytest自动化统计 -->
|
||||
<div class="stats-category" style="margin-top: 10px;">
|
||||
<div class="category-title">Pytest自动化</div>
|
||||
<a-grid :cols="4" :colGap="8" :rowGap="8">
|
||||
<a-grid-item>
|
||||
<a-statistic title="过程对象" :value="pytestStats.processObjects || 0" :value-style="{ color: '#165DFF' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="用例个数" :value="pytestStats.caseCount || 0" :value-style="{ color: '#F77234' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="工具文件" :value="pytestStats.toolFiles || 0" :value-style="{ color: '#00B42A' }" />
|
||||
</a-grid-item>
|
||||
<a-grid-item>
|
||||
<a-statistic title="测试文件" :value="pytestStats.testFiles || 0" :value-style="{ color: '#722ED1' }" />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
uiStats: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
elementCount: 0,
|
||||
pageCount: 0,
|
||||
stepCount: 0,
|
||||
caseCount: 0
|
||||
})
|
||||
},
|
||||
apiStats: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
interfaceCount: 0,
|
||||
caseCount: 0,
|
||||
headersCount: 0
|
||||
})
|
||||
},
|
||||
pytestStats: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
processObjects: 0,
|
||||
caseCount: 0,
|
||||
toolFiles: 0,
|
||||
testFiles: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 自动化测试统计容器样式
|
||||
.stats-container {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-fill-1);
|
||||
// 隐藏滚动条但保持滚动功能
|
||||
&::-webkit-scrollbar {
|
||||
display: none; // Chrome Safari
|
||||
}
|
||||
scrollbar-width: none; // Firefox
|
||||
-ms-overflow-style: none; // IE 10+
|
||||
}
|
||||
|
||||
|
||||
.stats-category {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-fill-1);
|
||||
|
||||
.category-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-statistic) {
|
||||
.arco-statistic-title {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-2);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.arco-statistic-content {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-grid-item) {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-fill-2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
mango-console/src/views/index/components/ContactAuthor.vue
Normal file
33
mango-console/src/views/index/components/ContactAuthor.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible" @cancel="handleCancel" @ok="handleOk" :width="800">
|
||||
<template #title> 联系作者</template>
|
||||
<div style="text-align: center">
|
||||
<p>进群需要GitHub点Starred(点星)</p>
|
||||
<a-space>
|
||||
<div>
|
||||
<p>作者微信</p>
|
||||
<img alt="作者微信" src="/static/images/author.jpg" style="width: 200px; height: 200px;" />
|
||||
</div>
|
||||
<div>
|
||||
<p>交流群</p>
|
||||
<img alt="交流群" src="/static/images/group.jpg" style="width: 200px; height: 200px;" />
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false })
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function handleOk() {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
93
mango-console/src/views/index/components/PendingTasks.vue
Normal file
93
mango-console/src/views/index/components/PendingTasks.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div style="flex: 1; overflow: auto; margin-top: 10px;">
|
||||
<a-table
|
||||
:bordered="true"
|
||||
:columns="tableColumns"
|
||||
:data="dataList"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:rowKey="rowKey"
|
||||
@selection-change="onSelectionChange"
|
||||
:scrollbar="true"
|
||||
:scroll="{
|
||||
y: 500
|
||||
}"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column
|
||||
v-for="item of tableColumns"
|
||||
:key="item.key"
|
||||
:align="item.align"
|
||||
:data-index="item.key"
|
||||
:ellipsis="item.ellipsis"
|
||||
:fixed="item.fixed"
|
||||
:title="item.title"
|
||||
:tooltip="item.tooltip"
|
||||
:width="item.width"
|
||||
>
|
||||
<template v-if="item.key === 'index'" #cell="{ record }">
|
||||
<span style="width: 110px; display: inline-block">{{ record.id }}</span>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'timing_strategy'" #cell="{ record }">
|
||||
{{ record.timing_strategy?.name }}
|
||||
</template>
|
||||
<template v-else-if="item.key === 'case_people'" #cell="{ record }">
|
||||
{{ record.case_people?.name }}
|
||||
</template>
|
||||
<template v-else-if="item.key === 'test_env'" #cell="{ record }">
|
||||
<a-tag :color="enumStore.colors[record.test_env]" size="small">
|
||||
{{
|
||||
record.test_env !== null
|
||||
? enumStore.environment_type[record.test_env].title
|
||||
: ''
|
||||
}}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'actions'" #cell="{ record }">
|
||||
<a-space>
|
||||
<a-button
|
||||
size="mini"
|
||||
type="text"
|
||||
class="custom-mini-btn"
|
||||
@click="onClick(record)"
|
||||
>查看结果
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRowKey, useRowSelection, useTableColumn } from '@/hooks/table'
|
||||
import { useEnum } from '@/store/modules/get-enum'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
dataList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tableColumns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['selection-change', 'view-result'])
|
||||
|
||||
const { onSelectionChange } = useRowSelection()
|
||||
const rowKey = useRowKey('id')
|
||||
const enumStore = useEnum()
|
||||
const router = useRouter()
|
||||
|
||||
function onClick(record: any) {
|
||||
emit('view-result', record)
|
||||
}
|
||||
</script>
|
||||
113
mango-console/src/views/index/components/ResourceCenter.vue
Normal file
113
mango-console/src/views/index/components/ResourceCenter.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div style="display: flex; flex-direction: column; gap: 15px; margin-top: 10px">
|
||||
<a-card class="resource-card" hoverable>
|
||||
<div class="resource-item" @click="handleDownloadExecutor">
|
||||
<icon-download class="resource-icon" />
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">执行器下载</div>
|
||||
<div class="resource-desc">下载最新版本的测试执行器</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="resource-card" hoverable>
|
||||
<div class="resource-item" @click="handleDownloadPlugin">
|
||||
<icon-download class="resource-icon" />
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">插件下载</div>
|
||||
<div class="resource-desc">获取各种功能扩展插件</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="resource-card" hoverable>
|
||||
<div class="resource-item" @click="handleViewHelp">
|
||||
<icon-book class="resource-icon" />
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">帮助文档</div>
|
||||
<div class="resource-desc">查看详细的使用说明</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card class="resource-card" hoverable>
|
||||
<div class="resource-item" @click="handleContactAuthor">
|
||||
<icon-user class="resource-icon" />
|
||||
<div class="resource-info">
|
||||
<div class="resource-title">联系作者</div>
|
||||
<div class="resource-desc">获取技术支持和反馈</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IconDownload, IconBook, IconUser } from '@arco-design/web-vue/es/icon'
|
||||
|
||||
const emit = defineEmits(['download-executor', 'download-plugin', 'view-help', 'contact-author'])
|
||||
|
||||
function handleDownloadExecutor() {
|
||||
emit('download-executor')
|
||||
}
|
||||
|
||||
function handleDownloadPlugin() {
|
||||
emit('download-plugin')
|
||||
}
|
||||
|
||||
function handleViewHelp() {
|
||||
emit('view-help')
|
||||
}
|
||||
|
||||
function handleContactAuthor() {
|
||||
emit('contact-author')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 资源中心卡片样式
|
||||
.resource-card {
|
||||
border: 1px solid var(--color-neutral-3);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
height: 50px;
|
||||
|
||||
// 移除悬浮效果
|
||||
&:hover {
|
||||
border-color: rgb(var(--primary-6));
|
||||
// 移除阴影和位移效果
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.resource-icon {
|
||||
font-size: 16px;
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
.resource-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.resource-desc {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
@@ -17,4 +17,4 @@ export default defineComponent({
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
24
mango-console/src/views/index/components/Title.vue
Normal file
24
mango-console/src/views/index/components/Title.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="title-container">
|
||||
<span class="line"></span>
|
||||
<span class="title">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Title',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './css/title.module.less';
|
||||
</style>
|
||||
@@ -7,10 +7,11 @@
|
||||
padding: 10px;
|
||||
|
||||
.title {
|
||||
color: var(--color-text-2);
|
||||
color: #333; /* 使用更深的颜色确保可见性 */
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
background-color: transparent; /* 确保背景透明 */
|
||||
}
|
||||
|
||||
.line {
|
||||
@@ -20,4 +21,4 @@
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,115 +10,84 @@
|
||||
<PieChart :chartData="data.reportSum" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<Title title="活跃度" />
|
||||
<HotProductChart ref="hotProductChart" />
|
||||
<Title title="自动化测试统计" />
|
||||
<AutomationStats
|
||||
:ui-stats="data.uiStats"
|
||||
:api-stats="data.apiStats"
|
||||
:pytest-stats="data.pytestStats"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<a-space direction="vertical" style="height: 100%">
|
||||
<a-space direction="vertical" style="height: 100%; display: flex; flex-direction: column">
|
||||
<a-card style="flex: 1; overflow: hidden">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<Title title="近3个月执行趋势图" />
|
||||
<FullYearSalesChart ref="fullYearSalesChart" />
|
||||
</div>
|
||||
</a-card>
|
||||
<a-card style="flex: 1; min-height: 300px; overflow: hidden">
|
||||
<div style="display: flex; flex-direction: column; height: 100%">
|
||||
<div style="flex: 0 0 auto">
|
||||
<Title style="height: 65px" title="正在准备执行的自动化任务" />
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<a-table
|
||||
:bordered="true"
|
||||
:columns="tableColumns"
|
||||
:data="table.dataList"
|
||||
:loading="table.tableLoading.value"
|
||||
:pagination="false"
|
||||
:rowKey="rowKey"
|
||||
@selection-change="onSelectionChange"
|
||||
:scrollbar="true"
|
||||
:scroll="{
|
||||
y: 310,
|
||||
}"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column
|
||||
v-for="item of tableColumns"
|
||||
:key="item.key"
|
||||
:align="item.align"
|
||||
:data-index="item.key"
|
||||
:ellipsis="item.ellipsis"
|
||||
:fixed="item.fixed"
|
||||
:title="item.title"
|
||||
:tooltip="item.tooltip"
|
||||
:width="item.width"
|
||||
>
|
||||
<template v-if="item.key === 'index'" #cell="{ record }">
|
||||
<span style="width: 110px; display: inline-block">{{ record.id }}</span>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'timing_strategy'" #cell="{ record }">
|
||||
{{ record.timing_strategy?.name }}
|
||||
</template>
|
||||
<template v-else-if="item.key === 'case_people'" #cell="{ record }">
|
||||
{{ record.case_people?.name }}
|
||||
</template>
|
||||
<template v-else-if="item.key === 'test_env'" #cell="{ record }">
|
||||
<a-tag :color="enumStore.colors[record.test_env]" size="small">
|
||||
{{
|
||||
record.test_env !== null
|
||||
? enumStore.environment_type[record.test_env].title
|
||||
: ''
|
||||
}}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'actions'" #cell="{ record }">
|
||||
<a-space>
|
||||
<a-button
|
||||
size="mini"
|
||||
type="text"
|
||||
class="custom-mini-btn"
|
||||
@click="onClick(record)"
|
||||
>查看结果
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
<div style="padding: 5px 5px 5px 5px">
|
||||
<TableFooter :pagination="pagination"
|
||||
/></div>
|
||||
</div>
|
||||
<a-card style="flex: 1; overflow: hidden; display: flex; flex-direction: column">
|
||||
<div style="flex: 0 0 auto">
|
||||
<Title title="正在准备执行的自动化任务" />
|
||||
</div>
|
||||
<PendingTasks
|
||||
:data-list="table.dataList"
|
||||
:loading="table.tableLoading.value"
|
||||
:table-columns="tableColumns"
|
||||
@selection-change="onSelectionChange"
|
||||
@view-result="handleViewResult"
|
||||
/>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="item">
|
||||
<Title title="资源中心" />
|
||||
<ResourceCenter
|
||||
@download-executor="downloadExecutor"
|
||||
@download-plugin="downloadPlugin"
|
||||
@view-help="viewHelp"
|
||||
@contact-author="contactAuthor"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<Title title="活跃度" />
|
||||
<HotProductChart ref="hotProductChart" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联系作者弹窗 -->
|
||||
<ContactAuthor v-model:visible="contactVisible" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref, unref, watch } from 'vue'
|
||||
import Title from '@/views/index/components/Title'
|
||||
import useAppConfigStore from '@/store/modules/app-config'
|
||||
import FullYearSalesChart from './components/chart/FullYearSalesChart.vue'
|
||||
import HotProductChart from './components/chart/HotProductChart.vue'
|
||||
import {
|
||||
usePagination,
|
||||
useRowKey,
|
||||
useRowSelection,
|
||||
useTable,
|
||||
useTableColumn,
|
||||
} from '@/hooks/table'
|
||||
import Title from '@/views/index/components/Title.vue'
|
||||
import { useRowKey, useRowSelection, useTable, useTableColumn } from '@/hooks/table'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getSystemTasks } from '@/api/system/tasks'
|
||||
import { useEnum } from '@/store/modules/get-enum'
|
||||
import PieChart from '@/components/chart/PieChart.vue'
|
||||
import { getSystemCaseRunSum, getSystemCaseSum } from '@/api/system'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
// 导入新创建的组件
|
||||
import ContactAuthor from './components/ContactAuthor.vue'
|
||||
import AutomationStats from './components/AutomationStats.vue'
|
||||
import PendingTasks from './components/PendingTasks.vue'
|
||||
import ResourceCenter from './components/ResourceCenter.vue'
|
||||
|
||||
const appStore = useAppConfigStore()
|
||||
|
||||
// 联系作者弹窗相关状态
|
||||
const contactVisible = ref(false)
|
||||
|
||||
const enumStore = useEnum()
|
||||
const hotProductChart = ref()
|
||||
const fullYearSalesChart = ref()
|
||||
@@ -135,46 +104,68 @@
|
||||
onResize()
|
||||
})
|
||||
|
||||
const pagination = usePagination(doRefresh)
|
||||
pagination.pageSize = 10
|
||||
|
||||
const { onSelectionChange } = useRowSelection()
|
||||
const table = useTable()
|
||||
const rowKey = useRowKey('id')
|
||||
const data: any = reactive({
|
||||
caseSum: [],
|
||||
reportSum: [],
|
||||
onlineUsers: 0,
|
||||
// UI自动化统计
|
||||
uiStats: {
|
||||
elementCount: 0,
|
||||
pageCount: 0,
|
||||
stepCount: 0,
|
||||
caseCount: 0,
|
||||
},
|
||||
// API自动化统计
|
||||
apiStats: {
|
||||
interfaceCount: 0,
|
||||
caseCount: 0,
|
||||
headersCount: 0,
|
||||
},
|
||||
// Pytest自动化统计
|
||||
pytestStats: {
|
||||
processObjects: 0,
|
||||
caseCount: 0,
|
||||
toolFiles: 0,
|
||||
testFiles: 0,
|
||||
},
|
||||
})
|
||||
const tableColumns = useTableColumn([
|
||||
table.indexColumn,
|
||||
|
||||
{
|
||||
title: '任务名称',
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '测试对象',
|
||||
key: 'test_env',
|
||||
dataIndex: 'test_env',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '定时策略',
|
||||
key: 'timing_strategy',
|
||||
dataIndex: 'timing_strategy',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '测试对象',
|
||||
key: 'test_env',
|
||||
dataIndex: 'test_env',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
key: 'case_people',
|
||||
dataIndex: 'case_people',
|
||||
width: 120,
|
||||
},
|
||||
])
|
||||
const router = useRouter()
|
||||
|
||||
function onClick(record: any) {
|
||||
function handleViewResult(record: any) {
|
||||
router.push({
|
||||
path: '/index/report-details',
|
||||
query: {
|
||||
@@ -185,16 +176,28 @@
|
||||
})
|
||||
}
|
||||
|
||||
function goToCreateCase() {
|
||||
router.push('/apitest/case/create')
|
||||
}
|
||||
|
||||
function goToTaskManagement() {
|
||||
router.push('/system/tasks')
|
||||
}
|
||||
|
||||
function goToReportCenter() {
|
||||
router.push('/index/report')
|
||||
}
|
||||
|
||||
function doRefresh() {
|
||||
getSystemTasks({
|
||||
pageSize: pagination.pageSize,
|
||||
page: pagination.page,
|
||||
pageSize: 100,
|
||||
page: 1,
|
||||
})
|
||||
.then((res) => {
|
||||
table.handleSuccess(res)
|
||||
pagination.setTotalSize((res as any).totalSize)
|
||||
caseSum()
|
||||
getAllReportSum()
|
||||
getSystemStatistics()
|
||||
})
|
||||
.catch(console.log)
|
||||
}
|
||||
@@ -215,18 +218,71 @@
|
||||
.catch(console.log)
|
||||
}
|
||||
|
||||
// 获取系统统计数据
|
||||
function getSystemStatistics() {
|
||||
// 这里可以添加实际的API调用来获取统计数据
|
||||
// 暂时使用模拟数据
|
||||
|
||||
// UI自动化统计
|
||||
data.uiStats.elementCount = 128
|
||||
data.uiStats.pageCount = 24
|
||||
data.uiStats.stepCount = 86
|
||||
data.uiStats.caseCount = 86
|
||||
|
||||
// API自动化统计
|
||||
data.apiStats.interfaceCount = 42
|
||||
data.apiStats.caseCount = 38
|
||||
data.apiStats.headersCount = 15
|
||||
|
||||
// Pytest自动化统计
|
||||
data.pytestStats.processObjects = 12
|
||||
data.pytestStats.caseCount = 56
|
||||
data.pytestStats.toolFiles = 8
|
||||
data.pytestStats.testFiles = 22
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(async () => {
|
||||
doRefresh()
|
||||
})
|
||||
})
|
||||
|
||||
// 资源中心相关方法
|
||||
function downloadExecutor() {
|
||||
// 跳转到执行器下载链接
|
||||
window.open('https://www.alipan.com/s/8CmZdabwt4R', '_blank')
|
||||
}
|
||||
|
||||
function downloadPlugin() {
|
||||
// 跳转到插件下载链接
|
||||
window.open('https://www.alipan.com/s/dEaiFz5Zvfq', '_blank')
|
||||
}
|
||||
|
||||
function viewHelp() {
|
||||
// 跳转到帮助文档链接
|
||||
window.open('http://43.142.161.61:8002/', '_blank')
|
||||
}
|
||||
|
||||
function contactAuthor() {
|
||||
// 打开联系作者弹窗
|
||||
contactVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.title-container) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.arco-card) {
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0px 8px 8px 0px rgba(162, 173, 200, 0.15);
|
||||
box-shadow: 0px 8px 16px 0px rgba(162, 173, 200, 0.2);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.arco-card:hover) {
|
||||
box-shadow: 0px 12px 20px 0px rgba(162, 173, 200, 0.3);
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
@@ -234,19 +290,31 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-modal) {
|
||||
.arco-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.arco-modal-footer {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
|
||||
.left {
|
||||
width: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
gap: 15px;
|
||||
|
||||
.item {
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@@ -254,6 +322,7 @@
|
||||
background: var(--color-bg-2);
|
||||
transition: box-shadow 0.2s cubic-bezier(0, 0, 1, 1);
|
||||
box-shadow: 0px 8px 8px 0px rgba(162, 173, 200, 0.15);
|
||||
padding: 12px;
|
||||
|
||||
div:nth-last-child(1) {
|
||||
flex: 1;
|
||||
@@ -261,26 +330,32 @@
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-top: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: 0 10px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.center-data-item-wrapper {
|
||||
:deep(.arco-space) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
margin: 10px 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
}
|
||||
:deep(.arco-card) {
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-left: 10px;
|
||||
}
|
||||
:deep(.arco-card:first-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
// 移除额外的margin-bottom
|
||||
:deep(.arco-card:last-child) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,13 +365,14 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
& > div:nth-child(1) {
|
||||
flex: 1;
|
||||
flex: 0.5;
|
||||
}
|
||||
|
||||
& > div:nth-child(2) {
|
||||
flex: 2;
|
||||
flex: 2.5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -306,9 +382,10 @@
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: var(--color-bg-2);
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s cubic-bezier(0, 0, 1, 1);
|
||||
box-shadow: 0px 8px 8px 0px rgba(162, 173, 200, 0.15);
|
||||
padding: 10px;
|
||||
|
||||
& > div:nth-child(2) {
|
||||
flex: 1;
|
||||
@@ -316,7 +393,74 @@
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-top: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式优化
|
||||
:deep(.arco-btn) {
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.arco-btn:hover) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 描述列表样式优化
|
||||
:deep(.arco-descriptions) {
|
||||
.arco-descriptions-item-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.arco-descriptions-item-value {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1200px) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
|
||||
.left,
|
||||
.center,
|
||||
.right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
flex-direction: row;
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
|
||||
.left,
|
||||
.right {
|
||||
flex-direction: column;
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
:deep(.arco-card:first-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user