优化部分页面的样式

This commit is contained in:
毛鹏
2025-11-09 00:28:58 +08:00
parent f4538714a6
commit 3e7f0b7205
14 changed files with 1304 additions and 281 deletions

View File

@@ -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:

View 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>

View File

@@ -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>

View File

@@ -111,6 +111,7 @@ export const useTableIndexColumn = function () {
key: 'index',
width: 100,
dataIndex: 'index',
align: 'center',
}
}

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -17,4 +17,4 @@ export default defineComponent({
</div>
)
},
})
})

View 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>

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}