This commit is contained in:
zpc 2026-02-23 22:11:44 +08:00
parent e1dc8c37ee
commit e85cd41a2d
3 changed files with 781 additions and 0 deletions

View File

@ -0,0 +1,137 @@
/**
* AssessmentRecord API - API
* @module api/business/assessmentRecord
* @description
*/
import { request, type ApiResponse } from '@/utils/request'
import type { PagedRequest, PagedResult } from '@/types/common'
// ==================== 类型定义 ====================
/** 测评记录列表项 */
export interface AssessmentRecordItem {
id: number
userId: number
userNickname: string | null
orderId: number
orderNo: string | null
assessmentTypeId: number
assessmentTypeName: string | null
name: string
phone: string
gender: number
genderName: string
age: number
educationStage: number
educationStageName: string
province: string
city: string
district: string
status: number
statusName: string
startTime: string | null
submitTime: string | null
completeTime: string | null
createTime: string
}
/** 答案详情 */
export interface AnswerDetail {
id: number
questionId: number
questionNo: number
questionContent: string
answerValue: number
createTime: string
}
/** 结果详情 */
export interface ResultDetail {
id: number
categoryId: number
categoryName: string
categoryTypeName: string
score: number
maxScore: number
percentage: number
rank: number
starLevel: number
createTime: string
}
/** 测评记录详情(含答案和结果) */
export interface AssessmentRecordDetail extends AssessmentRecordItem {
answers: AnswerDetail[]
results: ResultDetail[]
}
/** 报告分类项 */
export interface ReportCategoryItem {
categoryId: number
categoryName: string
score: number
maxScore: number
percentage: number
starLevel: number
conclusionContent: string | null
}
/** 报告分类组 */
export interface ReportCategoryGroup {
categoryTypeId: number
categoryTypeName: string
items: ReportCategoryItem[]
}
/** 测评报告 */
export interface AssessmentReport extends AssessmentRecordItem {
resultGroups: ReportCategoryGroup[]
}
/** 测评记录查询参数 */
export interface AssessmentRecordQuery extends PagedRequest {
userId?: number
assessmentTypeId?: number
status?: number
startDate?: string
endDate?: string
}
// ==================== API ====================
/** 获取测评记录列表 */
export function getRecordList(params: AssessmentRecordQuery): Promise<ApiResponse<PagedResult<AssessmentRecordItem>>> {
return request<PagedResult<AssessmentRecordItem>>({
url: '/admin/assessmentRecord/getList',
method: 'get',
params
})
}
/** 获取测评记录详情 */
export function getRecordDetail(id: number): Promise<ApiResponse<AssessmentRecordDetail>> {
return request<AssessmentRecordDetail>({
url: '/admin/assessmentRecord/getDetail',
method: 'get',
params: { id }
})
}
/** 获取测评报告 */
export function getRecordReport(id: number): Promise<ApiResponse<AssessmentReport>> {
return request<AssessmentReport>({
url: '/admin/assessmentRecord/getReport',
method: 'get',
params: { id }
})
}
/** 导出测评记录 */
export function exportRecords(params: AssessmentRecordQuery): Promise<ApiResponse<Blob>> {
return request<Blob>({
url: '/admin/assessmentRecord/export',
method: 'get',
params,
responseType: 'blob'
})
}

View File

@ -118,6 +118,12 @@ export const businessRoutes: RouteRecordRaw[] = [
name: 'ScoreOption',
component: () => import('@/views/business/assessment/scoreOption/index.vue'),
meta: { title: '评分标准', permission: 'assessment:view', keepAlive: true }
},
{
path: 'record',
name: 'AssessmentRecord',
component: () => import('@/views/business/assessment/record/index.vue'),
meta: { title: '测评记录', permission: 'assessmentRecord:view', keepAlive: true }
}
]
},

View File

@ -0,0 +1,638 @@
<template>
<div class="record-container">
<!-- 页面标题和操作栏 -->
<el-card class="page-header">
<div class="header-content">
<div class="header-left">
<h2 class="page-title">测评记录</h2>
<span class="page-description">查看用户测评记录答案详情和测评报告</span>
</div>
<div class="header-right">
<el-button type="success" @click="handleExport" :loading="state.exportLoading">
<el-icon><Download /></el-icon>
导出Excel
</el-button>
</div>
</div>
</el-card>
<!-- 搜索表单 -->
<el-card class="search-card">
<el-form :model="queryParams" inline>
<el-form-item label="用户ID">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户ID"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="待测评" :value="1" />
<el-option label="测评中" :value="2" />
<el-option label="生成中" :value="3" />
<el-option label="已完成" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="dateShortcuts"
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card v-loading="state.loading" class="table-card">
<el-table :data="state.tableData" row-key="id" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="用户" min-width="120">
<template #default="{ row }">
<div>{{ row.userNickname || '-' }}</div>
<div class="sub-text">ID: {{ row.userId }}</div>
</template>
</el-table-column>
<el-table-column label="被测评人" min-width="120">
<template #default="{ row }">
<div>{{ row.name }}</div>
<div class="sub-text">{{ row.phone }}</div>
</template>
</el-table-column>
<el-table-column prop="assessmentTypeName" label="测评类型" width="120" show-overflow-tooltip />
<el-table-column label="性别/年龄" width="100" align="center">
<template #default="{ row }">
{{ row.genderName }} / {{ row.age }}
</template>
</el-table-column>
<el-table-column prop="educationStageName" label="学历阶段" width="110" align="center" />
<el-table-column label="地区" min-width="130" show-overflow-tooltip>
<template #default="{ row }">
{{ row.province }}{{ row.city }}{{ row.district }}
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ row.statusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderNo" label="订单号" width="170" show-overflow-tooltip />
<el-table-column prop="submitTime" label="提交时间" width="170" align="center">
<template #default="{ row }">
{{ row.submitTime || '-' }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewDetail(row)">
<el-icon><View /></el-icon>
详情
</el-button>
<el-button
v-if="row.status === 4"
type="success"
link
size="small"
@click="handleViewReport(row)"
>
<el-icon><Document /></el-icon>
报告
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="state.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 详情抽屉 -->
<el-drawer
v-model="state.detailVisible"
title="测评记录详情"
size="650px"
:close-on-click-modal="true"
>
<div v-loading="state.detailLoading" class="record-detail">
<template v-if="state.detail">
<!-- 基本信息 -->
<div class="detail-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="被测评人">{{ state.detail.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ state.detail.phone }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ state.detail.genderName }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ state.detail.age }}</el-descriptions-item>
<el-descriptions-item label="学历阶段">{{ state.detail.educationStageName }}</el-descriptions-item>
<el-descriptions-item label="地区">{{ state.detail.province }}{{ state.detail.city }}{{ state.detail.district }}</el-descriptions-item>
<el-descriptions-item label="测评类型">{{ state.detail.assessmentTypeName }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(state.detail.status)" size="small">
{{ state.detail.statusName }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="订单号" :span="2">{{ state.detail.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ state.detail.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ state.detail.submitTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ state.detail.completeTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ state.detail.createTime }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 答案列表 -->
<div v-if="state.detail.answers && state.detail.answers.length > 0" class="detail-section">
<h4 class="section-title">答案列表{{ state.detail.answers.length }}</h4>
<el-table :data="state.detail.answers" stripe size="small" max-height="400">
<el-table-column prop="questionNo" label="题号" width="60" align="center" />
<el-table-column prop="questionContent" label="题目内容" min-width="250" show-overflow-tooltip />
<el-table-column prop="answerValue" label="答案" width="70" align="center">
<template #default="{ row }">
<el-tag size="small" type="primary">{{ row.answerValue }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 结果列表 -->
<div v-if="state.detail.results && state.detail.results.length > 0" class="detail-section">
<h4 class="section-title">测评结果</h4>
<el-table :data="state.detail.results" stripe size="small" max-height="400">
<el-table-column prop="categoryTypeName" label="分类类型" width="120" />
<el-table-column prop="categoryName" label="分类名称" min-width="120" />
<el-table-column label="得分" width="100" align="center">
<template #default="{ row }">
{{ row.score }} / {{ row.maxScore }}
</template>
</el-table-column>
<el-table-column label="百分比" width="80" align="center">
<template #default="{ row }">
{{ row.percentage }}%
</template>
</el-table-column>
<el-table-column label="星级" width="130" align="center">
<template #default="{ row }">
<el-rate v-model="row.starLevel" disabled />
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
</el-drawer>
<!-- 报告抽屉 -->
<el-drawer
v-model="state.reportVisible"
title="测评报告"
size="700px"
:close-on-click-modal="true"
>
<div v-loading="state.reportLoading" class="record-detail">
<template v-if="state.report">
<!-- 被测评人信息 -->
<div class="detail-section">
<h4 class="section-title">被测评人信息</h4>
<el-descriptions :column="3" border>
<el-descriptions-item label="姓名">{{ state.report.name }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ state.report.genderName }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ state.report.age }}</el-descriptions-item>
<el-descriptions-item label="学历阶段">{{ state.report.educationStageName }}</el-descriptions-item>
<el-descriptions-item label="地区" :span="2">{{ state.report.province }}{{ state.report.city }}{{ state.report.district }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 按分类分组展示结果 -->
<div
v-for="group in state.report.resultGroups"
:key="group.categoryTypeId"
class="detail-section"
>
<h4 class="section-title">{{ group.categoryTypeName }}</h4>
<el-table :data="group.items" stripe size="small">
<el-table-column prop="categoryName" label="分类" min-width="120" />
<el-table-column label="得分" width="100" align="center">
<template #default="{ row }">
{{ row.score }} / {{ row.maxScore }}
</template>
</el-table-column>
<el-table-column label="百分比" width="80" align="center">
<template #default="{ row }">
{{ row.percentage }}%
</template>
</el-table-column>
<el-table-column label="星级" width="130" align="center">
<template #default="{ row }">
<el-rate v-model="row.starLevel" disabled />
</template>
</el-table-column>
<el-table-column prop="conclusionContent" label="结论" min-width="200" show-overflow-tooltip />
</el-table>
</div>
</template>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
/**
* 测评记录管理页面
* @description 查看用户测评记录答案详情和测评报告支持搜索导出
*/
import { reactive, ref, onMounted } from 'vue'
import { Search, Refresh, View, Download, Document } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
getRecordList,
getRecordDetail,
getRecordReport,
exportRecords,
type AssessmentRecordItem,
type AssessmentRecordDetail,
type AssessmentReport,
type AssessmentRecordQuery
} from '@/api/business/assessmentRecord'
// ============ Constants ============
/** 日期快捷选项 */
const dateShortcuts = [
{
text: '最近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
}
},
{
text: '最近一个月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
}
},
{
text: '最近三个月',
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
return [start, end]
}
}
]
// ============ Types ============
interface RecordPageState {
loading: boolean
tableData: AssessmentRecordItem[]
total: number
detailVisible: boolean
detailLoading: boolean
detail: AssessmentRecordDetail | null
reportVisible: boolean
reportLoading: boolean
report: AssessmentReport | null
exportLoading: boolean
}
// ============ Refs ============
const dateRange = ref<[string, string] | null>(null)
// ============ State ============
const queryParams = reactive({
page: 1,
pageSize: 10,
userId: '',
status: undefined as number | undefined,
startDate: undefined as string | undefined,
endDate: undefined as string | undefined
})
const state = reactive<RecordPageState>({
loading: false,
tableData: [],
total: 0,
detailVisible: false,
detailLoading: false,
detail: null,
reportVisible: false,
reportLoading: false,
report: null,
exportLoading: false
})
// ============ Helper Functions ============
/**
* 获取状态标签类型
* 待测评=info, 测评中=primary, 生成中=warning, 已完成=success
*/
function getStatusTagType(status: number): 'info' | 'primary' | 'warning' | 'success' {
switch (status) {
case 1: return 'info'
case 2: return 'primary'
case 3: return 'warning'
case 4: return 'success'
default: return 'info'
}
}
// ============ API Functions ============
/** 加载测评记录列表 */
async function loadRecordList() {
state.loading = true
try {
const params: AssessmentRecordQuery = {
page: queryParams.page,
pageSize: queryParams.pageSize
}
if (queryParams.userId) {
params.userId = Number(queryParams.userId)
}
if (queryParams.status !== undefined) {
params.status = queryParams.status
}
if (queryParams.startDate) {
params.startDate = queryParams.startDate
}
if (queryParams.endDate) {
params.endDate = queryParams.endDate
}
const res = await getRecordList(params)
if (res.code === 0) {
state.tableData = res.data?.list || []
state.total = res.data?.total || 0
} else {
throw new Error(res.message || '获取测评记录列表失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '获取测评记录列表失败'
ElMessage.error(message)
} finally {
state.loading = false
}
}
/** 加载测评记录详情 */
async function loadRecordDetail(id: number) {
state.detailLoading = true
try {
const res = await getRecordDetail(id)
if (res.code === 0) {
state.detail = res.data
} else {
throw new Error(res.message || '获取测评记录详情失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '获取测评记录详情失败'
ElMessage.error(message)
} finally {
state.detailLoading = false
}
}
/** 加载测评报告 */
async function loadRecordReport(id: number) {
state.reportLoading = true
try {
const res = await getRecordReport(id)
if (res.code === 0) {
state.report = res.data
} else {
throw new Error(res.message || '获取测评报告失败')
}
} catch (error) {
const message = error instanceof Error ? error.message : '获取测评报告失败'
ElMessage.error(message)
} finally {
state.reportLoading = false
}
}
// ============ Event Handlers ============
function handleSearch() {
queryParams.page = 1
loadRecordList()
}
function handleReset() {
queryParams.userId = ''
queryParams.status = undefined
queryParams.startDate = undefined
queryParams.endDate = undefined
dateRange.value = null
queryParams.page = 1
loadRecordList()
}
function handleDateRangeChange(val: [string, string] | null) {
if (val) {
queryParams.startDate = val[0]
queryParams.endDate = val[1]
} else {
queryParams.startDate = undefined
queryParams.endDate = undefined
}
}
function handleSizeChange(size: number) {
queryParams.pageSize = size
queryParams.page = 1
loadRecordList()
}
function handleCurrentChange(page: number) {
queryParams.page = page
loadRecordList()
}
function handleViewDetail(row: AssessmentRecordItem) {
state.detailVisible = true
state.detail = null
loadRecordDetail(row.id)
}
function handleViewReport(row: AssessmentRecordItem) {
state.reportVisible = true
state.report = null
loadRecordReport(row.id)
}
async function handleExport() {
state.exportLoading = true
try {
const params: AssessmentRecordQuery = {
page: 1,
pageSize: 10000
}
if (queryParams.userId) {
params.userId = Number(queryParams.userId)
}
if (queryParams.status !== undefined) {
params.status = queryParams.status
}
if (queryParams.startDate) {
params.startDate = queryParams.startDate
}
if (queryParams.endDate) {
params.endDate = queryParams.endDate
}
const res = await exportRecords(params)
//
const blob = res.data instanceof Blob ? res.data : new Blob([res.data as BlobPart])
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `测评记录_${new Date().toISOString().slice(0, 10)}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
const message = error instanceof Error ? error.message : '导出失败'
ElMessage.error(message)
} finally {
state.exportLoading = false
}
}
// ============ Lifecycle ============
onMounted(() => {
loadRecordList()
})
</script>
<style scoped>
.record-container {
padding: 0;
}
.page-header {
margin-bottom: 16px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: baseline;
gap: 16px;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary, #303133);
}
.page-description {
font-size: 14px;
color: var(--text-secondary, #909399);
}
.search-card {
margin-bottom: 16px;
}
.search-card :deep(.el-card__body) {
padding-bottom: 2px;
}
.table-card {
min-height: 400px;
}
.sub-text {
font-size: 12px;
color: var(--text-secondary, #909399);
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
/* 详情/报告抽屉样式 */
.record-detail {
padding: 0 10px;
}
.detail-section {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #303133);
padding-bottom: 8px;
border-bottom: 1px solid var(--border-lighter, #ebeef5);
}
/* 表格样式 */
:deep(.el-table) {
--el-table-border-color: var(--border-lighter, #ebeef5);
}
:deep(.el-table th.el-table__cell) {
background-color: var(--bg-light, #f5f7fa);
font-weight: 500;
}
:deep(.el-descriptions) {
--el-descriptions-item-bordered-label-background: var(--bg-light, #f5f7fa);
}
</style>