This commit is contained in:
ChuXun
2025-10-25 19:18:43 +08:00
parent 4ce487588a
commit 02a830145e
3971 changed files with 1549956 additions and 2 deletions

View File

@@ -0,0 +1,607 @@
<template>
<el-dialog
:model-value="visible"
title="反馈详情"
width="70%"
:before-close="handleClose"
class="feedback-detail-dialog"
top="5vh"
>
<template #header>
<div class="dialog-header">
<span class="el-dialog__title">反馈详情</span>
<el-button
type="primary"
:icon="Refresh"
circle
@click="handleRefresh"
:loading="loading"
title="刷新数据"
/>
</div>
</template>
<div v-if="loading" v-loading="loading" class="loading-container"></div>
<div v-if="!loading && currentFeedback" class="detail-container">
<el-descriptions :column="2" border>
<el-descriptions-item label="ID">{{ currentFeedback.id }}</el-descriptions-item>
<el-descriptions-item label="事件编号">{{ currentFeedback.eventId || '无' }}</el-descriptions-item>
<el-descriptions-item label="标题" :span="2">{{ currentFeedback.title }}</el-descriptions-item>
<el-descriptions-item label="污染类型">{{ formatPollutionType(currentFeedback.pollutionType) }}</el-descriptions-item>
<el-descriptions-item label="严重程度">
<el-tag :type="getSeverityTagType(currentFeedback.severityLevel)">
{{ formatSeverityLevel(currentFeedback.severityLevel) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(currentFeedback.status)">
{{ formatStatus(currentFeedback.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDate(currentFeedback.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(currentFeedback.updatedAt) }}</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1" border class="mt-4">
<el-descriptions-item label="详细描述">
<div class="description-content">{{ currentFeedback.description }}</div>
</el-descriptions-item>
<el-descriptions-item label="提交位置" v-if="currentFeedback.latitude && currentFeedback.longitude">
{{ currentFeedback.textAddress || `经纬度: (${currentFeedback.longitude}, ${currentFeedback.latitude})` }}
</el-descriptions-item>
<el-descriptions-item label="报告人信息" v-if="currentFeedback.user">
{{ currentFeedback.user.name }}
<el-tag size="small" style="margin-left: 8px;">{{ formatRole(currentFeedback.user.role) }}</el-tag>
<div class="mt-1">ID: {{ currentFeedback.user.id }}, 电话: {{ currentFeedback.reporterPhone || '未提供' }}</div>
<div class="user-note mt-1" v-if="currentFeedback.user.role === 'GRID_WORKER'">
<el-alert
type="warning"
:closable="false"
show-icon
title="注意:通常反馈应由公众监督员提交,此处显示为网格员可能是测试数据"
/>
</div>
</el-descriptions-item>
</el-descriptions>
<div class="task-details mt-4" v-if="currentFeedback.task">
<h4>任务详情</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">{{ currentFeedback.task.id }}</el-descriptions-item>
<el-descriptions-item label="任务状态">
<el-tag :type="getStatusTagType(currentFeedback.task.status)">
{{ formatStatus(currentFeedback.task.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理人" v-if="currentFeedback.task.assignee">
{{ currentFeedback.task.assignee.name }} (ID: {{ currentFeedback.task.assignee.id }})
</el-descriptions-item>
<el-descriptions-item label="处理人电话" v-if="currentFeedback.task.assignee">
{{ currentFeedback.task.assignee.phone || '未提供' }}
</el-descriptions-item>
<el-descriptions-item label="网格坐标" :span="2" v-if="currentFeedback.task.gridX !== null && currentFeedback.task.gridY !== null">
X: {{ currentFeedback.task.gridX }}, Y: {{ currentFeedback.task.gridY }}
</el-descriptions-item>
<el-descriptions-item label="分配时间" v-if="currentFeedback.task.assignment?.assignmentTime">
{{ formatDate(currentFeedback.task.assignment.assignmentTime) }}
</el-descriptions-item>
<el-descriptions-item label="分配说明" v-if="currentFeedback.task.assignment?.remarks">
{{ currentFeedback.task.assignment.remarks }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 任务提交详情 -->
<div class="submission-details mt-4" v-if="currentFeedback.task && currentFeedback.task.submissionInfo">
<h4>任务提交详情</h4>
<el-descriptions :column="1" border>
<el-descriptions-item label="提交说明">
{{ currentFeedback.task.submissionInfo.comments }}
</el-descriptions-item>
<el-descriptions-item label="提交附件" v-if="currentFeedback.task.submissionInfo.attachments && currentFeedback.task.submissionInfo.attachments.length > 0">
<div v-for="att in currentFeedback.task.submissionInfo.attachments" :key="att.id">
<el-link :href="att.url" type="primary" target="_blank" :icon="Document">{{ att.fileName }}</el-link>
</div>
</el-descriptions-item>
<el-descriptions-item label="无附件" v-else>
未提交任何附件
</el-descriptions-item>
</el-descriptions>
</div>
<div class="image-gallery mt-4" v-if="currentFeedback.attachments && currentFeedback.attachments.length > 0">
<h4>相关图片</h4>
<el-image
v-for="att in currentFeedback.attachments"
:key="att.id"
:src="att.url"
:preview-src-list="currentFeedback.attachments.map(a => a.url)"
:initial-index="currentFeedback.attachments.findIndex(a => a.id === att.id)"
fit="cover"
class="feedback-image"
lazy
/>
</div>
</div>
<div v-if="!loading && !currentFeedback" class="empty-state">
<el-empty description="无法加载反馈详情" />
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<!-- 任务审批按钮 -->
<template v-if="currentFeedback?.task?.status === 'SUBMITTED' && canManageSubmittedTask">
<el-button type="success" @click="handleApprove" :loading="approving">批准通过</el-button>
<el-button type="danger" @click="handleRejectTask" :loading="rejecting">打回任务</el-button>
</template>
<!-- 根据状态条件性显示操作按钮 -->
<el-button type="primary" @click="handleProcess" v-if="canProcess">处理</el-button>
<el-button type="success" @click="handleAssign" v-if="canAssign">分配任务</el-button>
<el-button type="warning" @click="handleRejectFeedback" v-if="canReject">拒绝反馈</el-button>
</div>
</template>
</el-dialog>
<!-- 分配任务对话框 -->
<el-dialog
v-model="assignDialogVisible"
title="分配任务"
width="30%"
:before-close="() => assignDialogVisible = false"
>
<div v-if="loadingGridWorkers">正在加载网格员...</div>
<el-select
v-else
v-model="selectedGridWorkerId"
placeholder="请选择网格员"
style="width: 100%;"
>
<el-option
v-for="worker in gridWorkers"
:key="worker.id"
:label="`${worker.name} (ID: ${worker.id})${worker.phone ? ' - ' + worker.phone : ''}`"
:value="worker.id"
/>
</el-select>
<template #footer>
<span class="dialog-footer">
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAssign" :disabled="!selectedGridWorkerId">
确认分配
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed, onMounted, h } from 'vue';
import { useFeedbackStore } from '@/store/feedback';
import { storeToRefs } from 'pinia';
import { ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus';
import { FeedbackStatus, PollutionType, SeverityLevel, processFeedback, assignFeedback, rejectFeedback, resolveFeedback } from '@/api/feedback';
import { approveTask, rejectTask } from '@/api/tasks';
import type { UserAccount } from '@/api/types';
import apiClient from '@/api/index';
import { Document, Refresh } from '@element-plus/icons-vue';
import { useAuthStore } from '@/stores/auth';
const props = defineProps<{
visible: boolean;
feedbackId: number | null;
}>();
const emit = defineEmits(['close', 'refresh']);
const feedbackStore = useFeedbackStore();
const { currentFeedbackDetail: currentFeedback, detailLoading: loading } = storeToRefs(feedbackStore);
const authStore = useAuthStore();
const currentUser = computed(() => authStore.user);
const approving = ref(false);
const rejecting = ref(false);
// 添加网格员列表状态
const gridWorkers = ref<UserAccount[]>([]);
const loadingGridWorkers = ref(false);
const selectedGridWorkerId = ref<number | null>(null);
const assignDialogVisible = ref(false);
const handleRefresh = () => {
if (props.feedbackId) {
feedbackStore.fetchFeedbackDetail(props.feedbackId);
}
};
// 加载网格员列表
const loadGridWorkers = async () => {
loadingGridWorkers.value = true;
try {
// 直接调用API
const response = await apiClient.get('/tasks/grid-workers');
// 根据apiClient的拦截器response已经是处理过的数据了
gridWorkers.value = response as unknown as UserAccount[];
console.log('成功获取到网格员列表:', gridWorkers.value);
if (gridWorkers.value.length === 0) {
ElMessage.info('当前没有可用的网格员。');
}
} catch (error) {
console.error('加载网格员列表失败:', error);
ElMessage.error('加载网格员列表失败,请检查后端服务是否正常。');
gridWorkers.value = []; // 失败时清空数组
} finally {
loadingGridWorkers.value = false;
}
};
// 获取模拟网格员数据
const getMockGridWorkers = (): UserAccount[] => {
return [
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800000001',
role: 'GRID_WORKER',
status: 'ACTIVE',
gender: 'MALE',
region: '北京市朝阳区',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 2,
name: '李四',
email: 'lisi@example.com',
phone: '13800000002',
role: 'GRID_WORKER',
status: 'ACTIVE',
gender: 'FEMALE',
region: '北京市海淀区',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 3,
name: '王五',
email: 'wangwu@example.com',
phone: '13800000003',
role: 'GRID_WORKER',
status: 'ACTIVE',
gender: 'MALE',
region: '北京市西城区',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
];
};
// 在对话框显示时加载网格员列表
watch(
() => props.visible,
(newVisible) => {
if (newVisible && canAssign.value) {
// 移除这里的加载,改为在点击分配按钮时加载
}
}
);
// --- Computed Properties for Action Buttons ---
const canManageSubmittedTask = computed(() => {
// Per user requirements, only ADMINs can manage tasks submitted by Grid Workers.
return authStore.user?.role === 'ADMIN';
});
const isActionableBySupervisor = computed(() => {
// This property now simply checks if the user has the authority to perform
// pre-assignment actions like processing or rejecting feedback.
if (!authStore.user) return false;
const viewerRole = authStore.user.role;
return viewerRole === 'ADMIN' || viewerRole === 'SUPERVISOR';
});
const canProcess = computed(() => {
if (!currentFeedback.value || !isActionableBySupervisor.value) return false;
const processableStatus = [
FeedbackStatus.AI_REVIEWING,
FeedbackStatus.PENDING_REVIEW,
FeedbackStatus.CONFIRMED,
];
return processableStatus.includes(currentFeedback.value.status);
});
const canAssign = computed(() => {
if (!currentFeedback.value || !authStore.user) return false;
const viewerRole = authStore.user.role;
const isEligibleRole = viewerRole === 'ADMIN' || viewerRole === 'SUPERVISOR';
if (!isEligibleRole) {
return false;
}
// As per user feedback, supervisors and admins should be able to assign tasks.
// The button should appear when the feedback is ready for assignment.
// Based on the 'handleProcess' function, this status is PENDING_ASSIGNMENT.
return currentFeedback.value.status === 'PENDING_ASSIGNMENT';
});
const canReject = computed(() => {
if (!currentFeedback.value || !isActionableBySupervisor.value) return false;
const rejectableStatus = [
FeedbackStatus.AI_REVIEWING,
FeedbackStatus.PENDING_REVIEW,
];
return rejectableStatus.includes(currentFeedback.value.status);
});
// 格式化显示内容的辅助函数
const formatPollutionType = (type: string) => {
const map: Record<string, string> = {
'PM25': 'PM2.5',
'O3': '臭氧',
'NO2': '二氧化氮',
'SO2': '二氧化硫',
'OTHER': '其他'
};
return map[type] || type;
};
const formatSeverityLevel = (level: string) => {
const map: Record<string, string> = {
'LOW': '低',
'MEDIUM': '中',
'HIGH': '高'
};
return map[level] || level;
};
const formatStatus = (status: string) => {
const map: Record<string, string> = {
'AI_REVIEWING': 'AI审核中',
'PENDING_REVIEW': '待人工审核',
'REJECTED': '已拒绝',
'CONFIRMED': '已确认',
'ASSIGNED': '已分配',
'IN_PROGRESS': '处理中',
'RESOLVED': '已处理',
'CLOSED': '已关闭',
'PENDING_ASSIGNMENT': '待分配',
'PROCESSED': '已处理',
'COMPLETED': '已处理',
'SUBMITTED': '已提交'
};
return map[status] || status;
};
const getSeverityTagType = (level: string) => {
const map: Record<string, 'info' | 'warning' | 'danger'> = {
'LOW': 'info',
'MEDIUM': 'warning',
'HIGH': 'danger'
};
return map[level] || 'info';
};
const getStatusTagType = (status: string) => {
const map: Record<string, 'info' | 'warning' | 'danger' | 'primary' | 'success'> = {
'AI_REVIEWING': 'info',
'PENDING_REVIEW': 'warning',
'REJECTED': 'danger',
'CONFIRMED': 'primary',
'ASSIGNED': 'primary',
'IN_PROGRESS': 'warning',
'RESOLVED': 'success',
'CLOSED': 'info',
'PENDING_ASSIGNMENT': 'warning',
'PROCESSED': 'success',
'COMPLETED': 'success',
'SUBMITTED': 'info'
};
return map[status] || 'info';
};
const getFeedbackDisplayStatus = (feedback: any): string => {
if (!feedback) return '未知状态';
if (feedback.task && feedback.task.status === 'COMPLETED') {
return '已处理';
}
return formatStatus(feedback.status);
};
const getFeedbackDisplayTagType = (feedback: any): 'info' | 'warning' | 'danger' | 'primary' | 'success' => {
if (!feedback) return 'info';
if (feedback.task && feedback.task.status === 'COMPLETED') {
return 'success';
}
return getStatusTagType(feedback.status);
};
const formatDate = (dateStr: string) => {
try {
return new Date(dateStr).toLocaleString();
} catch (e) {
return dateStr;
}
};
const formatRole = (role: string) => {
const map: Record<string, string> = {
'ADMIN': '管理员',
'DECISION_MAKER': '决策人',
'SUPERVISOR': '监督员',
'GRID_WORKER': '网格员',
'PUBLIC_SUPERVISOR': '公众监督员'
};
return map[role] || role;
};
watch(
() => props.feedbackId,
(newId) => {
if (newId && props.visible) {
feedbackStore.fetchFeedbackDetail(newId);
}
},
{ immediate: true }
);
const handleClose = () => {
// 清空选中的网格员
selectedGridWorkerId.value = null;
emit('close');
};
const handleProcess = async () => {
if (!currentFeedback.value) return;
try {
await ElMessageBox.confirm(
'此操作将批准反馈并创建一个待分配的任务。',
'确认处理反馈',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'success',
}
);
await processFeedback(currentFeedback.value.id, {
status: FeedbackStatus.PENDING_ASSIGNMENT,
notes: '管理员已批准该反馈,准备分配任务。'
});
ElMessage.success('反馈已批准,进入待分配状态');
emit('refresh');
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('处理反馈失败');
}
}
};
const handleAssign = () => {
if (!currentFeedback.value) return;
loadGridWorkers();
assignDialogVisible.value = true;
};
const confirmAssign = async () => {
if (!currentFeedback.value || !selectedGridWorkerId.value) {
ElMessage.warning('请选择一个网格员');
return;
}
try {
await apiClient.post('/tasks/assign', {
feedbackId: currentFeedback.value.id,
assigneeId: selectedGridWorkerId.value
});
ElMessage.success('分配成功');
assignDialogVisible.value = false;
emit('refresh');
handleClose();
} catch (apiError) {
console.error('API分配失败:', apiError);
ElMessage.error('分配失败,请稍后重试');
}
};
const handleRejectTask = async () => {
const taskId = currentFeedback.value?.task?.id;
if (!taskId) {
ElMessage.error('无法获取任务ID操作取消');
return;
}
ElMessageBox.prompt('请输入打回任务的理由:', '打回任务', {
confirmButtonText: '确认打回',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '理由不能为空',
})
.then(async ({ value }) => {
rejecting.value = true;
try {
await rejectTask(taskId, value);
ElMessage.success('任务已打回');
emit('refresh');
handleClose();
} catch (error) {
console.error('打回任务失败:', error);
ElMessage.error('操作失败,请重试');
} finally {
rejecting.value = false;
}
})
.catch(() => {
ElMessage.info('已取消操作');
});
};
const handleApprove = async () => {
if (!currentFeedback.value?.task) return;
approving.value = true;
try {
await approveTask(currentFeedback.value.task.id);
ElMessage.success('任务已批准');
emit('refresh');
} catch (error) {
ElMessage.error('批准任务失败');
} finally {
approving.value = false;
}
};
const handleRejectFeedback = async () => {
if (!currentFeedback.value) return;
if (props.feedbackId) {
ElMessageBox.prompt('请输入拒绝此反馈的理由:', '拒绝反馈', {
confirmButtonText: '确认拒绝',
cancelButtonText: '取消',
})
.then(async ({ value }) => {
try {
await rejectFeedback(props.feedbackId!, { notes: value || '无明确理由' });
ElMessage.success('反馈已拒绝');
emit('refresh');
handleClose();
} catch (error) {
console.error('拒绝反馈失败:', error);
ElMessage.error('操作失败');
}
})
.catch(() => {
ElMessage.info('已取消拒绝操作');
});
}
};
</script>
<style scoped>
.feedback-detail-dialog .loading-container {
height: 300px;
}
.detail-container {
max-height: 75vh;
overflow-y: auto;
}
.mt-4 {
margin-top: 1.5rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.description-content {
white-space: pre-wrap;
}
.image-gallery {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.feedback-image {
width: 100px;
height: 100px;
border-radius: 4px;
cursor: pointer;
}
</style>