mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
feat: improve error handling and code quality
后端改进: - 添加统一异常处理系统 (exceptions.py, response.py) - 实现自定义异常类 (ValidationError, AuthorizationError, ResourceNotFoundError, BusinessLogicError) - 配置全局异常处理器,统一 API 错误响应格式 - 迁移业务逻辑错误到自定义异常 (users.py, auth.py) - 添加 SQL LIKE 通配符转义,防止通配符滥用 - 使用 EmailStr 进行邮箱格式验证 - 移除敏感字段暴露 (jwt_sub) 前端改进: - 配置 ESLint 9 (flat config) 和 Prettier - 修复所有 ESLint 错误和警告 - 移除未使用的变量和导入 - 为组件添加 PropTypes 默认值 - 统一代码格式和风格
This commit is contained in:
@@ -4,11 +4,7 @@
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<a-button
|
||||
@click="router.back()"
|
||||
type="link"
|
||||
class="mb-4 flex items-center"
|
||||
>
|
||||
<a-button type="link" class="mb-4 flex items-center" @click="router.back()">
|
||||
<template #icon><LeftOutlined /></template>
|
||||
返回任务列表
|
||||
</a-button>
|
||||
@@ -16,7 +12,9 @@
|
||||
<a-card v-if="currentTask" class="md3-card">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-gradient mb-2">{{ currentTask.name || '未命名任务' }}</h1>
|
||||
<h1 class="text-3xl font-bold text-gradient mb-2">
|
||||
{{ currentTask.name || '未命名任务' }}
|
||||
</h1>
|
||||
<div class="flex items-center gap-4 text-sm text-on-surface-variant">
|
||||
<span class="flex items-center">
|
||||
<NumberOutlined class="mr-1" />
|
||||
@@ -27,11 +25,7 @@
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="checkInLoading"
|
||||
@click="handleManualCheckIn"
|
||||
>
|
||||
<a-button type="primary" :loading="checkInLoading" @click="handleManualCheckIn">
|
||||
{{ checkInLoading ? '打卡中...' : '立即打卡' }}
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -49,31 +43,41 @@
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.05s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">成功次数</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">{{ recordStats.success }}</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{{ recordStats.success }}
|
||||
</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.1s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">时间范围外</p>
|
||||
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400">{{ recordStats.outOfTime }}</p>
|
||||
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{{ recordStats.outOfTime }}
|
||||
</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.15s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">失败次数</p>
|
||||
<p class="text-2xl font-bold text-red-600 dark:text-red-400">{{ recordStats.failure }}</p>
|
||||
<p class="text-2xl font-bold text-red-600 dark:text-red-400">
|
||||
{{ recordStats.failure }}
|
||||
</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.2s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">异常次数</p>
|
||||
<p class="text-2xl font-bold text-orange-600 dark:text-orange-400">{{ recordStats.unknown }}</p>
|
||||
<p class="text-2xl font-bold text-orange-600 dark:text-orange-400">
|
||||
{{ recordStats.unknown }}
|
||||
</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.25s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">成功率</p>
|
||||
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400">{{ recordStats.successRate }}%</p>
|
||||
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400">
|
||||
{{ recordStats.successRate }}%
|
||||
</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -83,7 +87,12 @@
|
||||
<a-space wrap :size="[16, 16]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-on-surface">状态筛选:</span>
|
||||
<a-radio-group v-model:value="filterStatus" button-style="solid" size="small" @change="handleFilterChange">
|
||||
<a-radio-group
|
||||
v-model:value="filterStatus"
|
||||
button-style="solid"
|
||||
size="small"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-radio-button value="">全部</a-radio-button>
|
||||
<a-radio-button value="success">成功</a-radio-button>
|
||||
<a-radio-button value="out_of_time">时间范围外</a-radio-button>
|
||||
@@ -94,7 +103,12 @@
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-on-surface">触发方式:</span>
|
||||
<a-radio-group v-model:value="filterTrigger" button-style="solid" size="small" @change="handleFilterChange">
|
||||
<a-radio-group
|
||||
v-model:value="filterTrigger"
|
||||
button-style="solid"
|
||||
size="small"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-radio-button value="">全部</a-radio-button>
|
||||
<a-radio-button value="scheduler">自动</a-radio-button>
|
||||
<a-radio-button value="manual">手动</a-radio-button>
|
||||
@@ -115,7 +129,11 @@
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-card v-else-if="records.length === 0" class="md3-card text-center" style="padding: 48px 20px;">
|
||||
<a-card
|
||||
v-else-if="records.length === 0"
|
||||
class="md3-card text-center"
|
||||
style="padding: 48px 20px"
|
||||
>
|
||||
<FileTextOutlined class="text-8xl text-on-surface-variant opacity-30 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-on-surface mb-2">暂无打卡记录</h3>
|
||||
<p class="text-on-surface-variant">当前筛选条件下没有找到任何打卡记录</p>
|
||||
@@ -130,25 +148,13 @@
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 class="text-lg font-semibold text-on-surface">
|
||||
打卡记录 #{{ record.id }}
|
||||
</h3>
|
||||
<a-tag
|
||||
v-if="record.status === 'success'"
|
||||
color="success"
|
||||
>✅ 打卡成功</a-tag>
|
||||
<a-tag
|
||||
v-else-if="record.status === 'out_of_time'"
|
||||
color="default"
|
||||
>🕐 时间范围外</a-tag>
|
||||
<a-tag
|
||||
v-else-if="record.status === 'unknown'"
|
||||
color="warning"
|
||||
>❗ 打卡异常</a-tag>
|
||||
<a-tag
|
||||
v-else
|
||||
color="error"
|
||||
>❌ 打卡失败</a-tag>
|
||||
<h3 class="text-lg font-semibold text-on-surface">打卡记录 #{{ record.id }}</h3>
|
||||
<a-tag v-if="record.status === 'success'" color="success">✅ 打卡成功</a-tag>
|
||||
<a-tag v-else-if="record.status === 'out_of_time'" color="default"
|
||||
>🕐 时间范围外</a-tag
|
||||
>
|
||||
<a-tag v-else-if="record.status === 'unknown'" color="warning">❗ 打卡异常</a-tag>
|
||||
<a-tag v-else color="error">❌ 打卡失败</a-tag>
|
||||
<a-tag :color="record.trigger_type === 'scheduled' ? 'blue' : 'orange'">
|
||||
{{ record.trigger_type === 'scheduled' ? '自动触发' : '手动触发' }}
|
||||
</a-tag>
|
||||
@@ -161,7 +167,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Record Details -->
|
||||
<div class="bg-surface-container-high dark:bg-surface-container rounded-lg p-4 space-y-2">
|
||||
<div
|
||||
class="bg-surface-container-high dark:bg-surface-container rounded-lg p-4 space-y-2"
|
||||
>
|
||||
<div v-if="record.response_text" class="flex items-start">
|
||||
<span class="text-sm font-medium text-on-surface-variant w-20">响应:</span>
|
||||
<span class="text-sm text-on-surface flex-1">{{ record.response_text }}</span>
|
||||
@@ -179,14 +187,14 @@
|
||||
<div v-if="!loading && records.length > 0" class="mt-6 flex justify-center">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
v-model:pageSize="pageSize"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:pageSizeOptions="['10', '20', '50', '100']"
|
||||
:page-size-options="['10', '20', '50', '100']"
|
||||
show-size-changer
|
||||
show-quick-jumper
|
||||
:show-total="total => `共 ${total} 条记录`"
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handleSizeChange"
|
||||
@show-size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,47 +203,47 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
LeftOutlined,
|
||||
NumberOutlined,
|
||||
FileTextOutlined,
|
||||
ClockCircleOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import Layout from '@/components/Layout.vue'
|
||||
import { useTaskStore } from '@/stores/task'
|
||||
import { formatDateTime } from '@/utils/helpers'
|
||||
} from '@ant-design/icons-vue';
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import { useTaskStore } from '@/stores/task';
|
||||
import { formatDateTime } from '@/utils/helpers';
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const taskStore = useTaskStore()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const taskStore = useTaskStore();
|
||||
|
||||
const taskId = computed(() => parseInt(route.params.taskId))
|
||||
const currentTask = ref(null)
|
||||
const records = ref([])
|
||||
const loading = ref(false)
|
||||
const checkInLoading = ref(false)
|
||||
const taskId = computed(() => parseInt(route.params.taskId));
|
||||
const currentTask = ref(null);
|
||||
const records = ref([]);
|
||||
const loading = ref(false);
|
||||
const checkInLoading = ref(false);
|
||||
|
||||
// Pagination
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
|
||||
// Filters
|
||||
const filterStatus = ref('')
|
||||
const filterTrigger = ref('')
|
||||
const filterStatus = ref('');
|
||||
const filterTrigger = ref('');
|
||||
|
||||
// Stats
|
||||
const recordStats = computed(() => {
|
||||
const success = records.value.filter(r => r.status === 'success').length
|
||||
const outOfTime = records.value.filter(r => r.status === 'out_of_time').length
|
||||
const failure = records.value.filter(r => r.status === 'failure').length
|
||||
const unknown = records.value.filter(r => r.status === 'unknown').length
|
||||
const totalRecords = records.value.length
|
||||
const successRate = totalRecords > 0 ? Math.round((success / totalRecords) * 100) : 0
|
||||
const success = records.value.filter(r => r.status === 'success').length;
|
||||
const outOfTime = records.value.filter(r => r.status === 'out_of_time').length;
|
||||
const failure = records.value.filter(r => r.status === 'failure').length;
|
||||
const unknown = records.value.filter(r => r.status === 'unknown').length;
|
||||
const totalRecords = records.value.length;
|
||||
const successRate = totalRecords > 0 ? Math.round((success / totalRecords) * 100) : 0;
|
||||
|
||||
return {
|
||||
total: totalRecords,
|
||||
@@ -244,129 +252,115 @@ const recordStats = computed(() => {
|
||||
failure,
|
||||
unknown,
|
||||
successRate,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// 从 payload_config 中提取 ThreadId
|
||||
const getThreadId = (task) => {
|
||||
if (!task || !task.payload_config) return '未知'
|
||||
const getThreadId = task => {
|
||||
if (!task || !task.payload_config) return '未知';
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(task.payload_config)
|
||||
return payload.ThreadId || '未知'
|
||||
const payload = JSON.parse(task.payload_config);
|
||||
return payload.ThreadId || '未知';
|
||||
} catch (e) {
|
||||
console.error('解析 payload_config 失败:', e)
|
||||
return '未知'
|
||||
console.error('解析 payload_config 失败:', e);
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取任务详情
|
||||
const fetchTaskDetail = async () => {
|
||||
try {
|
||||
currentTask.value = await taskStore.fetchTask(taskId.value)
|
||||
currentTask.value = await taskStore.fetchTask(taskId.value);
|
||||
} catch (error) {
|
||||
message.error(error.message || '获取任务详情失败')
|
||||
router.push('/tasks')
|
||||
message.error(error.message || '获取任务详情失败');
|
||||
router.push('/tasks');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取打卡记录
|
||||
const fetchRecords = async () => {
|
||||
loading.value = true
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
skip: (currentPage.value - 1) * pageSize.value,
|
||||
limit: pageSize.value,
|
||||
}
|
||||
};
|
||||
|
||||
if (filterStatus.value) {
|
||||
params.status = filterStatus.value
|
||||
params.status = filterStatus.value;
|
||||
}
|
||||
|
||||
if (filterTrigger.value) {
|
||||
params.trigger_type = filterTrigger.value
|
||||
params.trigger_type = filterTrigger.value;
|
||||
}
|
||||
|
||||
const response = await taskStore.fetchTaskRecords(taskId.value, params)
|
||||
const response = await taskStore.fetchTaskRecords(taskId.value, params);
|
||||
|
||||
// API 可能返回数组或对象
|
||||
if (Array.isArray(response)) {
|
||||
records.value = response
|
||||
total.value = response.length
|
||||
records.value = response;
|
||||
total.value = response.length;
|
||||
} else if (response.items) {
|
||||
records.value = response.items
|
||||
total.value = response.total || response.items.length
|
||||
records.value = response.items;
|
||||
total.value = response.total || response.items.length;
|
||||
} else {
|
||||
records.value = []
|
||||
total.value = 0
|
||||
records.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || '获取打卡记录失败')
|
||||
message.error(error.message || '获取打卡记录失败');
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 手动打卡
|
||||
const handleManualCheckIn = async () => {
|
||||
checkInLoading.value = true
|
||||
checkInLoading.value = true;
|
||||
|
||||
// 显示持久化通知
|
||||
const hide = message.loading('正在打卡中,请稍候... 您可以继续浏览其他页面', 0)
|
||||
const hide = message.loading('正在打卡中,请稍候... 您可以继续浏览其他页面', 0);
|
||||
|
||||
try {
|
||||
const result = await taskStore.checkInTask(taskId.value)
|
||||
hide()
|
||||
const result = await taskStore.checkInTask(taskId.value);
|
||||
hide();
|
||||
|
||||
if (result.success) {
|
||||
message.success('打卡成功')
|
||||
message.success('打卡成功');
|
||||
// 刷新记录列表
|
||||
await fetchRecords()
|
||||
await fetchRecords();
|
||||
} else {
|
||||
message.warning(result.message || '打卡失败')
|
||||
message.warning(result.message || '打卡失败');
|
||||
}
|
||||
} catch (error) {
|
||||
hide()
|
||||
message.error(error.message || '打卡失败')
|
||||
hide();
|
||||
message.error(error.message || '打卡失败');
|
||||
} finally {
|
||||
checkInLoading.value = false
|
||||
checkInLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 筛选变化
|
||||
const handleFilterChange = () => {
|
||||
currentPage.value = 1
|
||||
fetchRecords()
|
||||
}
|
||||
currentPage.value = 1;
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = () => {
|
||||
fetchRecords()
|
||||
}
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
currentPage.value = 1
|
||||
fetchRecords()
|
||||
}
|
||||
|
||||
// 格式化响应数据
|
||||
const formatResponse = (data) => {
|
||||
if (!data) return '-'
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
return JSON.stringify(parsed, null, 2).substring(0, 200) + (data.length > 200 ? '...' : '')
|
||||
} catch {
|
||||
return data.substring(0, 200) + (data.length > 200 ? '...' : '')
|
||||
}
|
||||
}
|
||||
return JSON.stringify(data, null, 2).substring(0, 200)
|
||||
}
|
||||
currentPage.value = 1;
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchTaskDetail()
|
||||
await fetchRecords()
|
||||
})
|
||||
await fetchTaskDetail();
|
||||
await fetchRecords();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user