609 lines
20 KiB
Vue
609 lines
20 KiB
Vue
<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': '公众监督员'
|
||
};
|
||
if (!role) return '未知';
|
||
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>
|