Selaa lähdekoodia

移植投诉管理

kagg886 2 kuukautta sitten
vanhempi
sitoutus
0c8e8b48b8

+ 8 - 0
src/api/system/report/complaint-resolve-history.ts

@@ -0,0 +1,8 @@
+import { ComplaintResolveHistory, ComplaintResolveHistoryInsertRequest } from '/@/api/system/report/type'
+import { get, post } from '/@/utils/request'
+
+export default {
+	list: (id: number): Promise<ComplaintResolveHistory[]> => get('/system/complaint/records',{ticketNo: id}).then((res: {data: ComplaintResolveHistory[]}) => res.data),
+
+	update:(params: ComplaintResolveHistoryInsertRequest) => post('/system/complaint/records/add', params)
+}

+ 25 - 0
src/api/system/report/complaints.ts

@@ -0,0 +1,25 @@
+import { get, post, del, put } from '/@/utils/request'
+import type {
+  Complaint,
+  CreateComplaintRequest,
+  UpdateComplaintRequest,
+  ComplaintQueryParams
+} from './type';
+
+export default {
+  // 获取投诉列表
+  getList: (params?: ComplaintQueryParams): Promise<{ list: Complaint[], total: number }> =>
+    get('/system/complaint/list', params),
+
+  // 创建投诉
+  add: (data: CreateComplaintRequest) => post('/system/complaint/add', data),
+
+  // 获取投诉详情
+  detail: (id: number): Promise<Complaint> => get(`/system/complaint/info`, {id}),
+
+  // 更新投诉
+  edit: (data: UpdateComplaintRequest) => put(`/system/complaint/edit`,data),
+
+  // 删除投诉
+  del: (ids: number[]) => del(`/system/complaint/delete`, {ids})
+};

+ 8 - 0
src/api/system/report/feedback.ts

@@ -0,0 +1,8 @@
+import { Feedback, FeedbackCreateParams, FeedbackQueryParams } from '/@/api/system/report/type'
+import { del, get, post } from '/@/utils/request'
+
+export default {
+	list: (params?: FeedbackQueryParams): Promise<{list: Feedback[],total: number}> => get('/system/complaintFeedback/list', params),
+	del: (ids: number[]): Promise<void> => del('/system/complaintFeedback/batch', {ids}),
+	create: (data: FeedbackCreateParams): Promise<void> => post('/system/complaintFeedback', data),
+}

+ 22 - 0
src/api/system/report/statistics.ts

@@ -0,0 +1,22 @@
+import { get } from '/@/utils/request';
+import type {
+  OverviewStatistics,
+  ComplaintTypeDistribution,
+  MonthlyTrend,
+  AreaDistribution,
+  StatisticsQueryParams
+} from './type';
+
+export default {
+  // 获取概要统计
+  overview: (params?: StatisticsQueryParams): Promise<OverviewStatistics> => get('/system/complaint/overview', params),
+
+  // 获取投诉类型分布
+  types: (): Promise<ComplaintTypeDistribution[]> => get('/system/complaint/types').then((res: {data: ComplaintTypeDistribution[]}) => res.data),
+
+  // 获取月度趋势
+  monthlyTrends: (): Promise<MonthlyTrend[]> => get('/system/complaint/monthly-trends').then((res: {data: MonthlyTrend[]}) => res.data),
+
+  // 获取区域分布
+  areas: (): Promise<AreaDistribution[]> => get('/system/complaint/areas').then((res: {data: AreaDistribution[]}) => res.data)
+};

+ 120 - 0
src/api/system/report/type.ts

@@ -0,0 +1,120 @@
+// 投诉区域枚举
+export type ComplaintArea = 'A区' | 'B区'
+
+export type ComplaintStatus = 'pending' | 'processing' | 'completed'
+
+// 投诉实体类型
+export interface Complaint {
+  id: number;
+  title: string;
+  category: string;
+  source: string;
+	area: ComplaintArea;
+	complainantName: string;
+	contact?: string;
+	level: string;
+	content: string;
+	assignee: number;
+	status: ComplaintStatus;
+
+  createdAt: string;
+  updatedAt: string;
+}
+
+export type CreateComplaintRequest = Pick<Complaint, 'title' | 'category' | 'source' | 'area' | 'complainantName' | 'contact' | 'level' | 'content'> & {
+	assignee?: number | null;
+}
+// 更新投诉请求类型
+export type UpdateComplaintRequest = CreateComplaintRequest & {
+  id: number;
+}
+
+export type BasePageQuery = {
+	pageNum?: number;
+	pageSize?: number;
+}
+
+// 投诉查询参数类型
+export type ComplaintQueryParams = BasePageQuery & {
+
+	dateRange?: [string, string]; //时间范围
+
+  name?: string; //关键词
+  status?: string; //状态
+  category?: string; //类型
+  level?: string; //等级
+
+	orderBy?: 'asc' | 'desc'; //排序方式
+}
+
+// 概要统计数据类型
+export interface OverviewStatistics {
+  totalComplaints: number;
+  pendingComplaints: number;
+  completedComplaints: number;
+  urgentComplaints: number;
+  averageProcessingTime: number;
+  completionRate: number;
+  satisfactionScore: number;
+  satisfactionTotal: number;
+}
+
+// 投诉类型分布数据类型
+export type ComplaintTypeDistribution = {
+  type: string;
+  count: number;
+  percentage: number;
+  trend: string;
+}
+
+// 月度趋势数据类型
+export type MonthlyTrend = {
+  month: string;
+  completionRate: number;
+  totalCount: number;
+  completedCount: number;
+}
+
+// 区域分布数据类型
+export type AreaDistribution = {
+  area: ComplaintArea;
+  count: number;
+  percentage: number;
+}
+
+// 统计查询参数类型
+export type StatisticsQueryParams = {
+  timeRange?: 'week' | 'month' | 'quarter' | 'year';
+}
+
+export type Feedback = {
+  id: number; // 反馈ID
+	surveyCode: string; // 问卷编号
+	ticketNo: number // 投诉编号
+	investigatorName: string; // 调查者姓名
+	contactInfo: string; // 联系信息
+	processingSpeed: string; // 处理速度(字典related_level)
+	staffAttitude: string; // 工作人员态度(字典related_level)
+	resolutionEffect: string // 解决效果(字典related_level)
+	otherSuggestions: string; // 其他建议
+	createdAt: string; // 创建时间
+}
+
+export type FeedbackQueryParams = BasePageQuery & {
+	investigatorName?: string;
+}
+
+export type FeedbackCreateParams = Omit<Feedback, 'id' | 'createdAt'>
+
+export type ComplaintResolveHistory = {
+	id: number;
+	ticketNo: number;
+	status: ComplaintStatus;
+	operator: string;
+	description: string
+	createdAt: string;
+	updatedAt: string
+}
+
+
+export type ComplaintResolveHistoryInsertRequest = Pick<ComplaintResolveHistory, 'status' | 'description' | 'ticketNo'>

+ 3 - 0
src/utils/delay.ts

@@ -0,0 +1,3 @@
+export function delay(ms: number) {
+  return new Promise(resolve => setTimeout(resolve, ms))
+}

+ 282 - 252
src/views/system/monitor/task-logs/index.vue

@@ -1,279 +1,309 @@
 <script setup lang="ts">
-import api from "/@/api/system";
-import { onMounted, reactive, ref } from "vue";
-import { Exception } from "sass";
-import { ElMessage, ElMessageBox } from "element-plus";
-import { useLoading } from "/@/utils/loading-util";
-import downloadFile from "/@/utils/download";
+import api from '/@/api/system'
+import { onMounted, reactive, ref } from 'vue'
+import { Exception } from 'sass'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useLoading } from '/@/utils/loading-util'
+import downloadFile from '/@/utils/download'
+
 // eslint-disable-next-line no-unused-vars
 enum StatusEnum {
-  // eslint-disable-next-line no-unused-vars
-  SUCCESS = 0,
-  // eslint-disable-next-line no-unused-vars
-  FAILED = 1,
+	// eslint-disable-next-line no-unused-vars
+	SUCCESS = 0,
+	// eslint-disable-next-line no-unused-vars
+	FAILED = 1,
 }
+
 type TaskLogSummary = {
-  id: number;
-  jobName: string;
-  invokeTarget: string;
-  cronExpression: string;
-  startTime: string; //开始时间
-  endTime?: string; //结束时间(仅success/failed拥有)
-  createdAt: string; //创建时间
-  status: StatusEnum;
-};
+	id: number
+	jobName: string
+	invokeTarget: string
+	cronExpression: string
+	startTime: string //开始时间
+	endTime?: string //结束时间(仅success/failed拥有)
+	createdAt: string //创建时间
+	status: StatusEnum
+}
+
+type TaskLogDetail = TaskLogSummary & {
+	exceptionInfo?: string //失败原因(仅failed拥有)
+	jobMessage?: string //结果(仅success拥有)
+}
+
 //数据搜索部分开始
 const searchParam = reactive<{
-  pageNum: number;
-  pageSize: number;
-  jobName?: string;
-  dateRange?: string[];
-  status?: StatusEnum;
+	pageNum: number
+	pageSize: number
+
+	jobName?: string
+	dateRange?: string[]
+	status?: StatusEnum
 }>({
-  pageNum: 1,
-  pageSize: 10,
-});
-const total = ref<number>(0);
-const data = ref<Array<TaskLogSummary>>([]);
+	pageNum: 1,
+	PageSize: 10,
+})
+
+const total = ref<number>(0)
+const data = ref<Array<TaskLogSummary>>([])
+
 const { loading, doLoading: doListLoad } = useLoading(async () => {
-  const res: {
-    list: TaskLogSummary[];
-    total: number;
-  } = await api.task_log.getList(searchParam).catch((ex: Exception) => {
-    // eslint-disable-next-line no-console
-    console.log(ex);
-    return {
-      list: [],
-      total: 0,
-    };
-  });
-  total.value = res.total;
-  data.value = res.list;
-});
+	const res: {
+		list: TaskLogSummary[];
+		total: number
+	} = await api.task_log.getList(searchParam).catch((ex: Exception) => {
+		// eslint-disable-next-line no-console
+		console.log(ex)
+		return {
+			list: [],
+			total: 0,
+		}
+	})
+	total.value = res.total
+	data.value = res.list
+})
+
 const reset = () => {
-  searchParam.pageNum = 1;
-  searchParam.pageSize = 10;
-  searchParam.jobName = undefined;
-  searchParam.dateRange = undefined;
-  searchParam.status = undefined;
-  doListLoad();
-};
-onMounted(doListLoad);
+	searchParam.pageNum = 1
+	searchParam.pageSize = 10
+	searchParam.jobName = undefined
+	searchParam.dateRange = undefined
+	searchParam.status = undefined
+	doListLoad()
+}
+onMounted(doListLoad)
+
 //数据搜索部分结束
+
 //数据选择删除部分开始
-const ids = ref<number[]>([]);
+
+const ids = ref<number[]>([])
 const onDeleteItemSelected = (row: TaskLogSummary[]) => {
-  ids.value = row.map((item) => item.id);
-};
+	ids.value = row.map((item) => item.id)
+}
+
 const del = async () => {
-  if (ids.value.length === 0) {
-    ElMessage.error("请选择要删除的数据");
-    return;
-  }
-  const confirm = await ElMessageBox.confirm("您确定要删除所选数据吗?", "提示", {
-    confirmButtonText: "确认",
-    cancelButtonText: "取消",
-    type: "warning",
-  });
-  if (confirm != "confirm") {
-    return;
-  }
-  const res = await api.task_log
-    .del(ids.value)
-    .then(() => true)
-    .catch((ex: Exception) => {
-      // eslint-disable-next-line no-console
-      console.log(ex);
-      return false;
-    });
-  if (!res) {
-    ElMessage.error("删除失败");
-  }
-  ElMessage.success("删除成功");
-  await doListLoad();
-  return;
-};
+	if (ids.value.length === 0) {
+		ElMessage.error('请选择要删除的数据')
+		return
+	}
+	const confirm = await ElMessageBox.confirm('您确定要删除所选数据吗?', '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+
+	if (confirm != 'confirm') {
+		return
+	}
+
+	const res = await api.task_log
+		.del(ids.value)
+		.then(() => true)
+		.catch((ex: Exception) => {
+			// eslint-disable-next-line no-console
+			console.log(ex)
+			return false
+		})
+
+	if (!res) {
+		ElMessage.error('删除失败')
+	}
+
+	ElMessage.success('删除成功')
+	await doListLoad()
+	return
+}
+
 const delSingle = async (id: number) => {
-  const confirm = await ElMessageBox.confirm("您确定要删除所选数据吗?", "提示", {
-    confirmButtonText: "确认",
-    cancelButtonText: "取消",
-    type: "warning",
-  });
-  if (confirm != "confirm") {
-    return;
-  }
-  const res = await api.task_log
-    .del([id])
-    .then(() => true)
-    .catch(() => {
-      return false;
-    });
-  if (!res) {
-    ElMessage.error("删除失败");
-  }
-  ElMessage.success("删除成功");
-  await doListLoad();
-  return;
-};
+	const confirm = await ElMessageBox.confirm('您确定要删除所选数据吗?', '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+
+	if (confirm != 'confirm') {
+		return
+	}
+	const res = await api.task_log
+		.del([id])
+		.then(() => true)
+		.catch((ex: Exception) => {
+			// eslint-disable-next-line no-console
+			console.log(ex)
+			return false
+		})
+
+	if (!res) {
+		ElMessage.error('删除失败')
+	}
+
+	ElMessage.success('删除成功')
+	await doListLoad()
+	return
+}
+
 //数据选择删除部分结束
+
 //导出日志部分开始
 const { loading: exportLoading, doLoading: doExport } = useLoading(async () => {
-  const res = await api.task_log.export(searchParam).catch((ex: Exception) => {
-    // eslint-disable-next-line no-console
-    console.log(ex);
-    return undefined;
-  });
-  if (!res) {
-    ElMessage.error("导出失败");
-    return;
-  }
-  ElMessage.success("导出成功");
-  downloadFile(res);
-});
+	const res = await api.task_log.export(searchParam).catch((ex: Exception) => {
+		// eslint-disable-next-line no-console
+		console.log(ex)
+		return undefined
+	})
+
+	if (!res) {
+		ElMessage.error('导出失败')
+		return
+	}
+
+	ElMessage.success('导出成功')
+	downloadFile(res)
+})
+
 //导出日志部分结束
+
 //查看日志详情部分开始
-const detailForm = ref<any>({
-  id: 0,
-  jobName: "",
-  invokeTarget: "",
-  cronExpression: "",
-  startTime: "",
-  endTime: "",
-  createdAt: "",
-  status: 0,
-  exceptionInfo: "",
-  jobMessage: "",
-});
-const detailDialogOpen = ref(false);
-const detailTabsNumber = ref<"0" | "1" | "2">("0");
+const detailForm = ref<TaskLogDetail | undefined>(undefined)
+const detailDialogOpen = ref(false)
+const detailTabsNumber = ref<'0' | '1' | '2'>('0')
+
 const { loading: detailLoading, doLoading: doDetailLoad } = useLoading(async (id: number) => {
-  detailForm.value = { id };
-  const res = await api.task_log.detail(id).catch((ex: Exception) => {
-    // eslint-disable-next-line no-console
-    console.log(ex);
-    return undefined;
-  });
-  if (!res) {
-    ElMessage.error("获取失败");
-    return;
-  }
-  detailForm.value = res;
-  detailDialogOpen.value = true;
-});
+	detailForm.value = {id} as TaskLogDetail
+	const res = await api.task_log.detail(id).catch((ex: Exception) => {
+		// eslint-disable-next-line no-console
+		console.log(ex)
+		return undefined
+	})
+	if (!res) {
+		ElMessage.error('获取失败')
+		return
+	}
+	detailForm.value = res
+	detailDialogOpen.value = true
+})
 </script>
 
 <template>
-  <el-card shadow="nover" class="page">
-    <el-form :model="searchParam" inline>
-      <el-form-item label="" prop="jobName">
-        <el-input style="width: 150px" v-model="searchParam.jobName" placeholder="任务名称"></el-input>
-      </el-form-item>
-      <el-form-item label="" prop="dateRange">
-        <el-date-picker v-model="searchParam.dateRange" style="width: 220px" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间"></el-date-picker>
-      </el-form-item>
-      <el-form-item label="" prop="status">
-        <el-select style="width: 125px" v-model="searchParam.status" placeholder="请选择">
-          <el-option label="全部" :value="undefined"></el-option>
-          <el-option label="成功" :value="StatusEnum.SUCCESS"></el-option>
-          <el-option label="失败" :value="StatusEnum.FAILED"></el-option>
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" class="ml10" @click="doListLoad">
-          <el-icon>
-            <ele-Search />
-          </el-icon>
-          查询
-        </el-button>
-        <el-button @click="reset">
-          <el-icon>
-            <ele-Refresh />
-          </el-icon>
-          重置
-        </el-button>
-
-        <el-button type="info" @click="del" v-auth="'del'">
-          <el-icon>
-            <ele-Delete />
-          </el-icon>
-          删除日志
-        </el-button>
-
-        <el-button type="primary" @click="doExport" v-loading="exportLoading" v-auth="'download'">
-          <el-icon>
-            <ele-Download />
-          </el-icon>
-          导出日志
-        </el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-table :data="data" style="width: 100%" v-loading="loading" @selection-change="onDeleteItemSelected">
-      <el-table-column type="selection" width="50" align="center"></el-table-column>
-      <el-table-column label="ID" prop="id" width="90" align="center" v-col="'jobId'"></el-table-column>
-      <el-table-column label="任务名称" prop="jobName" align="center" v-col="'jobName'"></el-table-column>
-      <el-table-column label="功能名称" prop="invokeTarget" align="center" v-col="'invokeTaget'"></el-table-column>
-      <el-table-column label="表达式" prop="cronExpression" align="center" v-col="'cronExpression'"></el-table-column>
-      <el-table-column label="开始时间" prop="startTime" align="center" v-col="'startTime'"></el-table-column>
-      <el-table-column label="结束时间" prop="endTime" align="center" v-col="'endTime'"></el-table-column>
-      <el-table-column label="创建时间" prop="createdAt" align="center" v-col="'createdAt'"></el-table-column>
-      <el-table-column label="状态" prop="status" align="center" v-col="'status'">
-        <template #default="scope">
-          <el-tag :type="scope.row.status === StatusEnum.SUCCESS ? 'success' : scope.row.status === StatusEnum.FAILED ? 'danger' : 'info'">
-            {{ scope.row.status === StatusEnum.SUCCESS ? "成功" : scope.row.status === StatusEnum.FAILED ? "失败" : "未知" }}
-          </el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" v-col="'handle'" width="180">
-        <template #default="scope">
-          <el-button type="text" size="small" v-auth="'detail'" @click="doDetailLoad(scope.row.id)" v-loading="detailLoading && detailForm.id === scope.row.id">
-            <el-icon>
-              <ele-Eye />
-            </el-icon>
-            查看
-          </el-button>
-          <el-button type="text" size="small" @click="delSingle(scope.row.id)" v-auth="'del'">删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination v-show="total > 0" :total="total" v-model:page="searchParam.pageNum" v-model:limit="searchParam.pageSize" @pagination="doListLoad" />
-
-    <el-dialog title="日志详情" v-model="detailDialogOpen" width="80%" destroy-on-close>
-      <el-tabs v-model="detailTabsNumber" type="border-card">
-        <el-tab-pane label="基本信息" name="0">
-          <el-form :model="detailForm" label-width="100px">
-            <el-form-item label="任务名称" prop="jobName">
-              <el-input style="width: 150px" v-model="detailForm.jobName" disabled></el-input>
-            </el-form-item>
-            <el-form-item label="功能名称" prop="invokeTarget">
-              <el-input style="width: 150px" v-model="detailForm.invokeTarget" disabled></el-input>
-            </el-form-item>
-            <el-form-item label="表达式" prop="cronExpression">
-              <el-input style="width: 150px" v-model="detailForm.cronExpression" disabled></el-input>
-            </el-form-item>
-            <el-form-item label="开始时间" prop="startTime">
-              <el-input style="width: 150px" v-model="detailForm.startTime" disabled></el-input>
-            </el-form-item>
-            <el-form-item label="结束时间" prop="endTime">
-              <el-input style="width: 150px" v-model="detailForm.endTime" disabled></el-input>
-            </el-form-item>
-            <el-form-item label="状态" prop="status">
-              <el-tag :type="detailForm?.status === StatusEnum.SUCCESS ? 'success' : detailForm?.status === StatusEnum.FAILED ? 'danger' : 'info'">
-                {{ detailForm?.status === StatusEnum.SUCCESS ? "成功" : detailForm?.status === StatusEnum.FAILED ? "失败" : "未知" }}
-              </el-tag>
-            </el-form-item>
-          </el-form>
-        </el-tab-pane>
-        <el-tab-pane label="结果" name="1">
-          {{ detailForm.jobMessage }}
-        </el-tab-pane>
-        <el-tab-pane label="失败原因" name="2">
-          {{ detailForm.exceptionInfo }}
-        </el-tab-pane>
-      </el-tabs>
-    </el-dialog>
-  </el-card>
+	<el-card shadow="nover" class="page">
+		<el-form :model="searchParam" inline>
+			<el-form-item label="" prop="jobName">
+				<el-input style="width: 150px" v-model="searchParam.jobName" placeholder="任务名称"></el-input>
+			</el-form-item>
+			<el-form-item label="" prop="dateRange">
+				<el-date-picker v-model="searchParam.dateRange" style="width: 220px" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间"></el-date-picker>
+			</el-form-item>
+			<el-form-item label="" prop="status">
+				<el-select style="width: 125px" v-model="searchParam.status" placeholder="请选择">
+					<el-option label="全部" :value="undefined"></el-option>
+					<el-option label="成功" :value="StatusEnum.SUCCESS"></el-option>
+					<el-option label="失败" :value="StatusEnum.FAILED"></el-option>
+				</el-select>
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" class="ml10" @click="doListLoad">
+					<el-icon>
+						<ele-Search />
+					</el-icon>
+					查询
+				</el-button>
+				<el-button @click="reset">
+					<el-icon>
+						<ele-Refresh />
+					</el-icon>
+					重置
+				</el-button>
+
+				<el-button type="info" @click="del" v-auth="'del'">
+					<el-icon>
+						<ele-Delete />
+					</el-icon>
+					删除日志
+				</el-button>
+
+				<el-button type="primary" @click="doExport" v-loading="exportLoading" v-auth="'download'">
+					<el-icon>
+						<ele-Download />
+					</el-icon>
+					导出日志
+				</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table :data="data" style="width: 100%" v-loading="loading" @selection-change="onDeleteItemSelected">
+			<el-table-column type="selection" width="50" align="center"></el-table-column>
+			<el-table-column label="ID" prop="id" width="90" align="center" v-col="'jobId'"></el-table-column>
+			<el-table-column label="任务名称" prop="jobName" align="center" v-col="'jobName'"></el-table-column>
+			<el-table-column label="功能名称" prop="invokeTarget" align="center" v-col="'invokeTaget'"></el-table-column>
+			<el-table-column label="表达式" prop="cronExpression" align="center" v-col="'cronExpression'"></el-table-column>
+			<el-table-column label="开始时间" prop="startTime" align="center" v-col="'startTime'"></el-table-column>
+			<el-table-column label="结束时间" prop="endTime" align="center" v-col="'endTime'"></el-table-column>
+			<el-table-column label="创建时间" prop="createdAt" align="center" v-col="'createdAt'"></el-table-column>
+			<el-table-column label="状态" prop="status" align="center" v-col="'status'">
+				<template #default="scope">
+					<el-tag
+						:type="scope.row.status === StatusEnum.SUCCESS ? 'success' : scope.row.status === StatusEnum.FAILED ? 'danger' : 'info'">
+						{{
+							scope.row.status === StatusEnum.SUCCESS ? '成功' : scope.row.status === StatusEnum.FAILED ? '失败' : '未知'
+						}}
+					</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" align="center" width="180">
+				<template #default="scope">
+					<el-button type="text" size="small" @click="doDetailLoad(scope.row.id)" v-loading="detailLoading && detailForm.id === scope.row.id">
+						<el-icon>
+							<ele-Eye />
+						</el-icon>
+						查看
+					</el-button>
+					<el-button type="text" size="small" @click="delSingle(scope.row.id)" v-auth="'del'">删除</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<pagination v-show="total > 0" :total="total" v-model:page="searchParam.pageNum"
+								v-model:limit="searchParam.pageSize" @pagination="doListLoad" />
+
+		<el-dialog title="日志详情" v-model="detailDialogOpen" width="80%" destroy-on-close>
+			<el-tabs v-model="detailTabsNumber" type="border-card">
+				<el-tab-pane label="基本信息" name="0">
+					<el-form :model="detailForm" label-width="100px">
+						<el-form-item label="任务名称" prop="jobName">
+							<el-input style="width: 150px" v-model="detailForm.jobName" disabled></el-input>
+						</el-form-item>
+						<el-form-item label="功能名称" prop="invokeTarget">
+							<el-input style="width: 150px" v-model="detailForm.invokeTarget" disabled></el-input>
+						</el-form-item>
+						<el-form-item label="表达式" prop="cronExpression">
+							<el-input style="width: 150px" v-model="detailForm.cronExpression" disabled></el-input>
+						</el-form-item>
+						<el-form-item label="开始时间" prop="startTime">
+							<el-input style="width: 150px" v-model="detailForm.startTime" disabled></el-input>
+						</el-form-item>
+						<el-form-item label="结束时间" prop="endTime">
+							<el-input style="width: 150px" v-model="detailForm.endTime" disabled></el-input>
+						</el-form-item>
+						<el-form-item label="状态" prop="status">
+							<el-tag
+								:type="detailForm?.status === StatusEnum.SUCCESS?'success' : detailForm?.status === StatusEnum.FAILED? 'danger' : 'info'">
+								{{
+									detailForm?.status === StatusEnum.SUCCESS ? '成功' : detailForm?.status === StatusEnum.FAILED ? '失败' : '未知'
+								}}
+							</el-tag>
+						</el-form-item>
+					</el-form>
+				</el-tab-pane>
+				<el-tab-pane label="结果" name="1">
+					{{detailForm.jobMessage}}
+				</el-tab-pane>
+				<el-tab-pane label="失败原因" name="2">
+					{{detailForm.exceptionInfo}}
+				</el-tab-pane>
+			</el-tabs>
+		</el-dialog>
+
+	</el-card>
 </template>
 
 <style scoped lang="scss"></style>

+ 627 - 0
src/views/system/report/componments/report-detail-dialog.vue

@@ -0,0 +1,627 @@
+<script setup lang="ts">
+import { computed, getCurrentInstance, ref, unref, watch } from 'vue'
+import { Complaint, ComplaintResolveHistoryInsertRequest } from '/@/api/system/report/type'
+import { useAsyncState } from '@vueuse/core'
+import complaint_resolve_history from '/@/api/system/report/complaint-resolve-history'
+import complaints from '/@/api/system/report/complaints'
+import { useLoading } from '/@/utils/loading-util'
+import { ElMessage } from 'element-plus'
+
+const { proxy } = getCurrentInstance() as any
+
+//投诉等级,投诉来源,投诉类型
+const {
+	report_level,
+	report_type,
+	report_source,
+}: {
+	[key: string]: Array<{
+		label: string
+		value: string
+	}>
+} = proxy.useDict('report_level', 'report_type', 'report_source')
+
+// eslint-disable-next-line no-unused-vars
+const formatReportLevel = computed<(value: string) => string>(() => {
+	const levels = unref(report_level)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (levels === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(levels, value)
+	}
+})
+
+// eslint-disable-next-line no-unused-vars
+const formatReportType = computed<(value: string) => string>(() => {
+	const types = unref(report_type)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (types === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(types, value)
+	}
+})
+
+// eslint-disable-next-line no-unused-vars
+const formatReportSource = computed<(value: string) => string>(() => {
+	const sources = unref(report_source)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (sources === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(sources, value)
+	}
+})
+
+const props = defineProps<{
+	id: number | undefined
+
+	visible: boolean
+}>()
+
+const emit = defineEmits<{
+	// eslint-disable-next-line no-unused-vars
+	(e: 'update:visible', val: boolean): void
+}>()
+
+const visible = computed({
+	get: () => props.visible,
+	set: (val) => emit('update:visible', val),
+})
+
+const {
+	state: complaintDetail,
+	isLoading: complaintDetailLoading,
+	execute: complaintDetailExecute,
+} = useAsyncState<Complaint | undefined>(async () => {
+	if (props.id === undefined) {
+		return undefined
+	}
+	return complaints.detail(props.id)
+}, undefined)
+
+const {
+	state: complaintResolveList,
+	isLoading: isComplaintResolveLoading,
+	execute: complaintResolveListExecute,
+} = useAsyncState(async () => {
+	if (props.id === undefined) {
+		return []
+	}
+	return complaint_resolve_history.list(props.id)
+}, [])
+
+watch(
+	() => props.id,
+	() => {
+		if (props.id !== undefined) {
+			complaintDetailExecute(0)
+			complaintResolveListExecute(0)
+		}
+	}
+)
+
+const currentResolveStatus = computed(() => complaintResolveList.value.at(-1) ?? undefined)
+
+const formatReportStatus = (value: Complaint['status']) => {
+	let a = '-'
+	switch (value) {
+		case 'completed':
+			a = '已完成'
+			break
+		case 'pending':
+			a = '待处理'
+			break
+		case 'processing':
+			a = '进行中'
+			break
+	}
+	return a
+}
+
+const showUpdateForm = ref(false)
+const formComplaintResolve = ref<Omit<ComplaintResolveHistoryInsertRequest, 'ticketNo'>>({
+	description: '',
+	status: 'processing',
+})
+
+const handleCancelUpdate = () => {
+	showUpdateForm.value = false
+	formComplaintResolve.value = {
+		description: '',
+		status: 'processing',
+	}
+}
+
+const { loading: createComplaintResolveLoading, doLoading: createComplaintResolve } = useLoading(async () => {
+	const valid = formComplaintResolve.value?.description?.trim()
+	if (!valid) {
+		ElMessage.error('请输入处理描述')
+		return
+	}
+	const result = await complaint_resolve_history
+		.update({
+			ticketNo: complaintDetail.value?.id!,
+			...formComplaintResolve.value,
+		})
+		.then(() => true)
+		.catch(() => false)
+
+	if (!result) {
+		return
+	}
+	ElMessage.success('处理状态更新成功')
+	formComplaintResolve.value = {
+		description: '',
+		status: 'processing',
+	}
+	showUpdateForm.value = false
+})
+</script>
+
+<template>
+	<el-dialog v-model="visible" title="投诉详情" width="800px" :close-on-click-modal="false" @close="() => visible = false">
+		<div class="complaint-detail">
+			<!-- 头部信息 -->
+			<div class="complaint-header" v-loading="complaintDetailLoading">
+				<div class="complaint-id">
+					<span class="id-text">#{{ complaintDetail?.id ?? '-1' }}</span>
+					<el-tag :type="complaintDetail?.level === '1' ? 'danger' : complaintDetail?.level === '2' ? 'warning' : 'info'" class="ml-2">
+						{{ formatReportLevel(complaintDetail?.level) }}
+					</el-tag>
+					<el-tag
+						:type="complaintDetail?.status === 'pending' ? 'info' : complaintDetail?.status === 'processing' ? 'warning' : 'success'"
+						class="ml-2"
+					>
+						{{ formatReportStatus(complaintDetail?.status) }}
+					</el-tag>
+				</div>
+				<h2 class="complaint-title">{{ complaintDetail?.title ?? '-' }}</h2>
+				<div class="complaint-time">
+					<el-icon>
+						<ele-Clock />
+					</el-icon>
+					<span>创建时间:{{ complaintDetail?.createdAt ?? '-' }}</span>
+					<el-icon class="ml-4">
+						<ele-Refresh />
+					</el-icon>
+					<span>更新时间:{{ complaintDetail?.updatedAt ?? '-' }}</span>
+				</div>
+			</div>
+
+			<el-row :gutter="20" class="mt-4" >
+				<!-- 左侧基本信息 -->
+				<el-col :span="14">
+					<el-card shadow="never" class="info-card">
+						<template #header>
+							<div class="card-header">
+								<el-icon>
+									<ele-Document />
+								</el-icon>
+								<span>基本信息</span>
+							</div>
+						</template>
+
+						<div v-loading="complaintDetailLoading">
+							<el-row :gutter="16" class="info-row" >
+								<el-col :span="12">
+									<div class="info-item">
+										<span class="info-label">投诉类型</span>
+										<span class="info-value">{{ formatReportType(complaintDetail?.category) }}</span>
+									</div>
+								</el-col>
+								<el-col :span="12">
+									<div class="info-item">
+										<span class="info-label">投诉来源</span>
+										<span class="info-value">{{ formatReportSource(complaintDetail?.source) }}</span>
+									</div>
+								</el-col>
+							</el-row>
+
+							<el-row :gutter="16" class="info-row">
+								<el-col :span="12">
+									<div class="info-item">
+										<span class="info-label">投诉区域</span>
+										<span class="info-value">{{ complaintDetail?.area }}</span>
+									</div>
+								</el-col>
+								<el-col :span="12">
+									<div class="info-item">
+										<span class="info-label">负责人</span>
+										<span class="info-value">{{ complaintDetail?.assignee ?? '-' }}</span>
+									</div>
+								</el-col>
+							</el-row>
+						</div>
+					</el-card>
+
+					<!-- 投诉内容 -->
+					<el-card shadow="never" class="info-card mt-4">
+						<template #header>
+							<div class="card-header">
+								<el-icon>
+									<ele-ChatDotRound />
+								</el-icon>
+								<span>投诉内容</span>
+							</div>
+						</template>
+						<div class="complaint-content" v-loading="complaintDetailLoading">
+							{{ complaintDetail?.content ?? '' }}
+						</div>
+					</el-card>
+
+					<!-- 处理记录 -->
+					<el-card shadow="never" class="info-card mt-4">
+						<template #header>
+							<div class="card-header">
+								<el-icon>
+									<ele-Clock />
+								</el-icon>
+								<span>处理记录</span>
+							</div>
+						</template>
+						<el-timeline v-loading="isComplaintResolveLoading">
+							<el-timeline-item v-for="(record, index) in complaintResolveList" :key="index">
+								<div class="timeline-content">
+									<div class="timeline-header">
+										<span class="timeline-title">{{ formatReportStatus(record.status) }}</span>
+										<span class="timeline-operator">by {{ record.operator }}</span>
+									</div>
+									<p class="timeline-desc">{{ record.description }}</p>
+									<span class="timeline-time">{{ record.createdAt }}</span>
+								</div>
+							</el-timeline-item>
+						</el-timeline>
+					</el-card>
+				</el-col>
+
+				<!-- 右侧投诉人信息 -->
+				<el-col :span="10">
+					<el-card shadow="never" class="info-card">
+						<template #header>
+							<div class="card-header">
+								<el-icon>
+									<ele-User />
+								</el-icon>
+								<span>投诉人信息</span>
+							</div>
+						</template>
+
+						<div class="complainant-info">
+							<div class="complainant-name">{{ complaintDetail?.complainantName ?? '-' }}</div>
+							<div class="complainant-contact">
+								<el-icon>
+									<ele-Phone />
+								</el-icon>
+								<span>{{ complaintDetail?.contact ?? '暂无' }}</span>
+							</div>
+							<div class="complainant-area">
+								<el-icon>
+									<ele-Location />
+								</el-icon>
+								<span>{{ complaintDetail?.area ?? '-' }}</span>
+							</div>
+						</div>
+					</el-card>
+
+					<!-- 处理操作区域 -->
+					<el-card shadow="never" class="info-card mt-4" v-if="!showUpdateForm">
+						<template #header>
+							<div class="card-header">
+								<el-icon>
+									<ele-Warning />
+								</el-icon>
+								<span>处理操作</span>
+							</div>
+						</template>
+
+						<div class="process-info">
+							<div class="current-status">
+								<span class="status-label">当前状态</span>
+								<div class="status-value">{{ formatReportStatus(currentResolveStatus?.status) }}</div>
+							</div>
+
+							<div class="process-note">
+								<span class="note-label">处理备注</span>
+								<div class="note-content">{{ currentResolveStatus?.description }}</div>
+							</div>
+
+							<el-button type="primary" size="large" class="update-btn" @click="showUpdateForm = true"> 更新处理状态 </el-button>
+						</div>
+					</el-card>
+
+					<!-- 更新处理状态表单 -->
+					<el-card shadow="never" class="info-card mt-4" v-else>
+						<template #header>
+							<div class="card-header">
+								<el-icon>
+									<ele-Warning />
+								</el-icon>
+								<span>处理操作</span>
+							</div>
+						</template>
+
+						<div class="update-form">
+							<div class="form-item">
+								<span class="form-label">更新状态</span>
+								<el-select v-model="formComplaintResolve.status" placeholder="选择状态" style="width: 100%">
+									<el-option label="待处理" value="pending" />
+									<el-option label="处理中" value="processing" />
+									<el-option label="已完成" value="completed" />
+								</el-select>
+							</div>
+
+							<div class="form-item">
+								<span class="form-label">处理备注</span>
+								<el-input
+									v-model="formComplaintResolve.description"
+									type="textarea"
+									:rows="4"
+									placeholder="请输入处理备注..."
+									maxlength="500"
+									show-word-limit
+								/>
+							</div>
+
+							<div class="form-actions">
+								<el-button type="primary" :loading="createComplaintResolveLoading" @click="createComplaintResolve">
+									<el-icon>
+										<ele-Document />
+									</el-icon>
+									保存
+								</el-button>
+								<el-button @click="handleCancelUpdate">取消</el-button>
+							</div>
+						</div>
+					</el-card>
+				</el-col>
+			</el-row>
+		</div>
+	</el-dialog>
+</template>
+
+<style scoped lang="scss">
+.complaint-detail {
+	.complaint-header {
+		border-bottom: 1px solid #ebeef5;
+		padding-bottom: 16px;
+		margin-bottom: 16px;
+
+		.complaint-id {
+			display: flex;
+			align-items: center;
+			margin-bottom: 8px;
+
+			.id-text {
+				font-size: 18px;
+				font-weight: bold;
+				color: #409eff;
+			}
+		}
+
+		.complaint-title {
+			font-size: 20px;
+			font-weight: 600;
+			color: #303133;
+			margin: 8px 0;
+		}
+
+		.complaint-time {
+			display: flex;
+			align-items: center;
+			color: #909399;
+			font-size: 14px;
+
+			.el-icon {
+				margin-right: 4px;
+			}
+		}
+	}
+
+	.info-card {
+		border: 1px solid #ebeef5;
+		border-radius: 8px;
+
+		.card-header {
+			display: flex;
+			align-items: center;
+			font-weight: 600;
+			color: #303133;
+
+			.el-icon {
+				margin-right: 8px;
+				color: #409eff;
+			}
+		}
+
+		.info-row {
+			margin-bottom: 16px;
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+		}
+
+		.info-item {
+			display: flex;
+			flex-direction: column;
+			margin-bottom: 12px;
+
+			.info-label {
+				font-size: 14px;
+				color: #909399;
+				margin-bottom: 4px;
+			}
+
+			.info-value {
+				min-height: 20px;
+				font-size: 14px;
+				color: #303133;
+				font-weight: 500;
+			}
+		}
+
+		.complaint-content {
+			padding: 16px;
+			background-color: #f8f9fa;
+			border-radius: 6px;
+			line-height: 1.6;
+			color: #303133;
+			min-height: 80px;
+		}
+	}
+
+	.complainant-info {
+		text-align: center;
+		padding: 20px;
+
+		.complainant-name {
+			font-size: 24px;
+			font-weight: 600;
+			color: #303133;
+			margin-bottom: 16px;
+		}
+
+		.complainant-contact,
+		.complainant-area {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			margin-bottom: 12px;
+			color: #606266;
+
+			.el-icon {
+				margin-right: 8px;
+				color: #909399;
+			}
+		}
+	}
+
+	.process-info {
+		padding: 20px;
+
+		.current-status {
+			margin-bottom: 20px;
+
+			.status-label {
+				font-size: 14px;
+				color: #909399;
+				display: block;
+				margin-bottom: 8px;
+			}
+
+			.status-value {
+				font-size: 18px;
+				font-weight: 600;
+				color: #303133;
+			}
+		}
+
+		.process-note {
+			margin-bottom: 24px;
+
+			.note-label {
+				font-size: 14px;
+				color: #909399;
+				display: block;
+				margin-bottom: 8px;
+			}
+
+			.note-content {
+				padding: 12px;
+				background-color: #f8f9fa;
+				border-radius: 6px;
+				color: #606266;
+				line-height: 1.5;
+				min-height: 60px;
+			}
+		}
+
+		.update-btn {
+			width: 100%;
+			height: 48px;
+			font-size: 16px;
+			font-weight: 600;
+			//background-color: #303133;
+			//border-color: #303133;
+			//
+			//&:hover {
+			//	background-color: #404040;
+			//	border-color: #404040;
+			//}
+		}
+	}
+
+	.update-form {
+		padding: 20px;
+
+		.form-item {
+			margin-bottom: 20px;
+
+			.form-label {
+				font-size: 14px;
+				color: #303133;
+				font-weight: 500;
+				display: block;
+				margin-bottom: 8px;
+			}
+		}
+
+		.form-actions {
+			display: flex;
+			gap: 12px;
+			margin-top: 24px;
+
+			.el-button {
+				flex: 1;
+				height: 40px;
+			}
+		}
+	}
+
+	.timeline-content {
+		flex: 1;
+		padding-bottom: 16px;
+
+		.timeline-header {
+			display: flex;
+			align-items: center;
+			gap: 8px;
+			margin-bottom: 4px;
+
+			.timeline-title {
+				font-size: 14px;
+				font-weight: 500;
+				color: #303133;
+			}
+
+			.timeline-operator {
+				font-size: 12px;
+				color: #909399;
+			}
+		}
+
+		.timeline-desc {
+			margin: 0 0 4px 0;
+			color: #606266;
+			line-height: 1.5;
+			font-size: 14px;
+		}
+
+		.timeline-time {
+			font-size: 12px;
+			color: #c0c4cc;
+		}
+	}
+}
+</style>

+ 211 - 0
src/views/system/report/feedback/index.vue

@@ -0,0 +1,211 @@
+<script setup lang="ts">
+import feedback from '/@/api/system/report/feedback'
+import { computed, getCurrentInstance, ref, unref } from 'vue'
+import { useAsyncState } from '@vueuse/core'
+import { Feedback, FeedbackQueryParams } from '/@/api/system/report/type'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { Search } from '@element-plus/icons-vue'
+
+const { proxy } = getCurrentInstance() as any
+
+//投诉等级,投诉来源,投诉类型
+const { related_level }: {
+	[key: string]: Array<{
+		label: string
+		value: string
+	}>
+} = proxy.useDict('related_level')
+
+// eslint-disable-next-line no-unused-vars
+const formatRelatedLevel = computed<(value: string) => string>(() => {
+	const levels = unref(related_level)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (levels === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(levels, value)
+	}
+})
+
+const queryRef = ref()
+const params = ref<FeedbackQueryParams>({
+	pageNum: 1,
+	pageSize: 10,
+	investigatorName: ''
+})
+
+const {state, isLoading, execute} = useAsyncState(
+	async () => feedback.list(params.value),
+	{total: 0, list: []}
+)
+
+// 查询
+const handleQuery = () => {
+	params.value.pageNum = 1
+	execute()
+}
+
+// 重置查询
+const resetQuery = () => {
+	if (queryRef.value) {
+		queryRef.value.resetFields()
+	}
+	params.value = {
+		pageNum: 1,
+		pageSize: 10,
+	}
+	execute()
+}
+
+// 分页改变
+const handlePagination = ({ page, limit }: { page: number; limit: number }) => {
+	params.value.pageNum = page
+	params.value.pageSize = limit
+	execute()
+}
+
+// 删除操作
+const handleDelete = (row: Feedback) => {
+	ElMessageBox.confirm(`此操作将永久删除问卷编号:"${row.surveyCode}"的反馈,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+		.then(() => {
+			feedback.del([row.id]).then(() => {
+				ElMessage.success('删除成功')
+				execute()
+			})
+		})
+		.catch(() => {})
+}
+
+const deleteIds = ref<number[]>([])
+
+// 表格选择改变事件
+const handleSelectionChange = (selection: Feedback[]) => {
+	deleteIds.value = selection.map(item => item.id)
+}
+
+// 批量删除
+const handleBatchDelete = async () => {
+	if (deleteIds.value.length === 0) {
+		ElMessage.warning('请选择要删除的数据')
+		return
+	}
+
+	const action = await ElMessageBox.confirm(`确定要删除选中的 ${deleteIds.value.length} 条反馈吗?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).catch(() => 'cancel')
+
+	if (action !== 'confirm') {
+		return
+	}
+
+	const result = await feedback.del(deleteIds.value).then(() => true).catch(() => false)
+
+	if (result) {
+		ElMessage.success('删除成功')
+		deleteIds.value = []
+		await execute()
+	} else {
+		ElMessage.error('删除失败')
+	}
+}
+</script>
+
+<template>
+	<div class="page">
+		<el-card shadow="never">
+			<el-form :model="params" ref="queryRef" inline>
+				<el-form-item label="" prop="investigatorName">
+					<el-input
+						v-model="params.investigatorName"
+						placeholder="调查者姓名搜索"
+						clearable
+						style="width: 200px"
+						@keyup.enter="handleQuery"
+					/>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" :icon="Search" @click="handleQuery">
+						查询
+					</el-button>
+					<el-button @click="resetQuery">
+						重置
+					</el-button>
+					<el-button 
+						type="danger" 
+						@click="handleBatchDelete" 
+						:disabled="deleteIds.length === 0"
+					>
+						批量删除
+					</el-button>
+				</el-form-item>
+			</el-form>
+
+			<el-table
+				:data="state.list"
+				style="width: 100%"
+				v-loading="isLoading"
+				stripe
+				@selection-change="handleSelectionChange"
+			>
+				<el-table-column type="selection" width="55" align="center" />
+				<el-table-column type="index" label="序号" width="60" align="center" />
+				<el-table-column prop="surveyCode" label="问卷编号" width="150" show-overflow-tooltip />
+				<el-table-column prop="ticketNo" label="投诉编号" width="120" align="center" />
+				<el-table-column prop="investigatorName" label="调查者姓名" width="120" show-overflow-tooltip />
+				<el-table-column prop="contactInfo" label="联系信息" width="150" show-overflow-tooltip />
+				<el-table-column prop="processingSpeed" label="处理速度" width="120" align="center">
+					<template #default="scope">
+						{{ formatRelatedLevel(scope.row.processingSpeed) }}
+					</template>
+				</el-table-column>
+				<el-table-column prop="staffAttitude" label="工作人员态度" width="140" align="center">
+					<template #default="scope">
+						{{ formatRelatedLevel(scope.row.staffAttitude) }}
+					</template>
+				</el-table-column>
+				<el-table-column prop="resolutionEffect" label="解决效果" width="120" align="center">
+					<template #default="scope">
+						{{ formatRelatedLevel(scope.row.resolutionEffect) }}
+					</template>
+				</el-table-column>
+				<el-table-column prop="otherSuggestions" label="其他建议" min-width="200" show-overflow-tooltip />
+				<el-table-column prop="createdAt" label="创建时间" width="180" align="center" />
+				<el-table-column label="操作" width="100" align="center" fixed="right">
+					<template #default="scope">
+						<el-button
+							size="small"
+							text
+							type="danger"
+							@click="handleDelete(scope.row)"
+						>
+							删除
+						</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<pagination
+				v-show="state.total > 0"
+				:total="state.total"
+				v-model:page="params.pageNum"
+				v-model:limit="params.pageSize"
+				@pagination="handlePagination"
+			/>
+		</el-card>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.page {
+	padding: 20px;
+}
+</style>

+ 923 - 0
src/views/system/report/list/index.vue

@@ -0,0 +1,923 @@
+<script setup lang="ts">
+import { ref, onMounted, getCurrentInstance, unref, computed, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search, Plus, Delete, View, Edit } from '@element-plus/icons-vue'
+import { useLoading } from '/@/utils/loading-util'
+import complaints from '/@/api/system/report/complaints'
+import feedback_api from '/@/api/system/report/feedback'
+import {
+	ComplaintQueryParams,
+	CreateComplaintRequest,
+	ComplaintArea,
+	UpdateComplaintRequest,
+	Complaint,
+	FeedbackCreateParams,
+} from '/@/api/system/report/type'
+import system from '/@/api/system'
+import { useAsyncState, useLocalStorage } from '@vueuse/core'
+import ReportDetailDialog from '/@/views/system/report/componments/report-detail-dialog.vue'
+import { MessageParamsWithType, MessageProps } from 'element-plus/es/components/message/src/message'
+
+const { proxy } = getCurrentInstance() as any
+
+//投诉等级,投诉来源,投诉类型
+const {
+	report_level,
+	report_source,
+	report_type,
+}: {
+	[key: string]: Array<{
+		label: string
+		value: string
+	}>
+} = proxy.useDict('report_level', 'report_source', 'report_type')
+
+// eslint-disable-next-line no-unused-vars
+const formatReportLevel = computed<(value: string) => string>(() => {
+	const levels = unref(report_level)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (levels === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(levels, value)
+	}
+})
+
+// eslint-disable-next-line no-unused-vars
+const formatReportType = computed<(value: string) => string>(() => {
+	const types = unref(report_type)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (types === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(types, value)
+	}
+})
+const formatReportStatus = (value: Complaint['status']) => {
+	let a = '-'
+	switch (value) {
+		case 'completed':
+			a = '已完成'
+			break
+		case 'pending':
+			a = '待处理'
+			break
+		case 'processing':
+			a = '进行中'
+			break
+	}
+	return a
+}
+
+const formatReportStatusTagType = (value: Complaint['status']) => {
+	let type: MessageProps['type'] | undefined = undefined
+	switch (value) {
+		case 'completed':
+			type = 'success'
+			break
+		case 'pending':
+			type = 'info'
+			break
+		case 'processing':
+			type = undefined
+			break
+	}
+	return type
+}
+
+// 响应式数据
+const tableData = ref<Complaint[]>([])
+const total = ref(0)
+const selectedIds = ref<number[]>([])
+const queryRef = ref()
+
+// 查询参数
+const queryParams = ref<ComplaintQueryParams>({
+	pageNum: 1,
+	pageSize: 20,
+})
+
+// 获取列表数据
+const { loading, doLoading: getComplaintList } = useLoading(async () => {
+	const data = await complaints.getList(queryParams.value).catch(() => undefined)
+
+	if (!data) {
+		return
+	}
+
+	tableData.value = data.list
+	total.value = data.total
+})
+
+// 事件处理
+const handleSearch = () => {
+	queryParams.value.pageNum = 1
+	getComplaintList()
+}
+
+const handlePageChange = ({ page, limit }: { page: number; limit: number }) => {
+	queryParams.value.pageNum = page
+	queryParams.value.pageSize = limit
+	getComplaintList()
+}
+
+const handleSelectionChange = (selection: Complaint[]) => {
+	selectedIds.value = selection.map((item) => item.id)
+}
+
+// 新增投诉相关
+const addDialogVisible = ref(false)
+const addFormRef = ref()
+const addForm = ref<CreateComplaintRequest>({
+	title: '',
+	category: '',
+	source: '',
+	area: 'A区' as ComplaintArea,
+	complainantName: '',
+	contact: '',
+	level: '',
+	content: '',
+	assignee: undefined,
+})
+
+//草稿,若有内容则会patch上去
+//添加flag的原因是:useLocalStorage不允许初值为undefined
+//flag为undefined证明有草稿,否则无草稿。
+type CCRWithDemoFlag = Partial<CreateComplaintRequest> & {
+	flag: boolean | undefined
+}
+
+const demo = useLocalStorage<CCRWithDemoFlag>('system-report-summary-index-demo-params',{
+	flag: false
+})
+
+onMounted(()=> {
+	if (demo.value.flag !== undefined) {
+		addForm.value = {
+			...(demo.value as CreateComplaintRequest)
+		}
+	}
+})
+
+// 表单验证规则
+const addFormRules = {
+	title: [{ required: true, message: '请输入投诉标题', trigger: 'blur' }],
+	category: [{ required: true, message: '请选择投诉类型', trigger: 'change' }],
+	source: [{ required: true, message: '请选择投诉来源', trigger: 'change' }],
+	area: [{ required: true, message: '请选择投诉区域', trigger: 'change' }],
+	complainantName: [{ required: true, message: '请输入投诉人姓名', trigger: 'blur' }],
+	level: [{ required: true, message: '请选择投诉等级', trigger: 'change' }],
+	content: [{ required: true, message: '请输入投诉内容', trigger: 'blur' }],
+}
+
+const handleAdd = () => {
+	addDialogVisible.value = true
+}
+
+const handleAddCancel = () => {
+	addDialogVisible.value = false
+	addFormRef.value?.resetFields()
+}
+
+const handleDemoAdded = () => {
+	demo.value = {
+		...addForm.value,
+		flag: true
+	}
+	ElMessage.success('草稿已经保存')
+}
+
+const handleAddConfirm = async () => {
+	const valid = await addFormRef.value?.validate().catch(() => false)
+	if (!valid) {
+		return
+	}
+
+	const result = await complaints
+		.add(addForm.value)
+		.then(() => true)
+		.catch(() => false)
+
+	if (result) {
+		ElMessage.success('新增投诉成功')
+
+		//删除草稿
+		demo.value = {
+			flag: undefined
+		}
+
+		//关闭新增对话框
+		addDialogVisible.value = false
+		addFormRef.value?.resetFields()
+		await getComplaintList()
+	} else {
+		ElMessage.error('新增投诉失败')
+	}
+}
+
+//修改投诉相关
+const editDialogVisible = ref(false)
+const editFormRef = ref()
+const editForm = ref<UpdateComplaintRequest | undefined>(undefined)
+const editFormRules = {
+	title: [{ required: true, message: '请输入投诉标题', trigger: 'blur' }],
+	category: [{ required: true, message: '请选择投诉类型', trigger: 'change' }],
+	source: [{ required: true, message: '请选择投诉来源', trigger: 'change' }],
+	area: [{ required: true, message: '请选择投诉区域', trigger: 'change' }],
+	complainantName: [{ required: true, message: '请输入投诉人姓名', trigger: 'blur' }],
+	level: [{ required: true, message: '请选择投诉等级', trigger: 'change' }],
+	content: [{ required: true, message: '请输入投诉内容', trigger: 'blur' }],
+}
+
+const currentLoadingEdit = ref(-1)
+const { loading: loadingEdit, doLoading: handleEdit } = useLoading(async (id: number) => {
+	currentLoadingEdit.value = id
+	const data = await complaints.detail(id).catch(() => undefined)
+	if (!data) {
+		return
+	}
+	editForm.value = data
+	editDialogVisible.value = true
+})
+
+const handleEditCancel = () => {
+	editDialogVisible.value = false
+	editFormRef.value?.resetFields()
+	currentLoadingEdit.value = -1
+}
+
+const handleEditConfirm = async () => {
+	const valid = await editFormRef.value?.validate().catch(() => false)
+	if (!valid) {
+		return
+	}
+	const data = editForm.value
+	if (!data) {
+		return
+	}
+	const result = await complaints
+		.edit(data)
+		.then(() => true)
+		.catch(() => false)
+
+	if (result) {
+		ElMessage.success('修改投诉成功')
+		editDialogVisible.value = false
+		editFormRef.value?.resetFields()
+
+		await getComplaintList()
+	}
+}
+const handleDeleteSingle = async (row: Complaint) => {
+	const status = await ElMessageBox.confirm(`确定要删除投诉 "${row.title}" 吗?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+
+	if (status !== 'confirm') {
+		return
+	}
+
+	const result = await complaints
+		.del([row.id])
+		.then(() => true)
+		.catch(() => false)
+
+	if (result) {
+		ElMessage.success('删除成功')
+		await getComplaintList()
+	}
+}
+
+const handleDelete = async () => {
+	if (selectedIds.value.length === 0) {
+		ElMessage.warning('请选择要删除的数据')
+		return
+	}
+
+	const status = await ElMessageBox.confirm(`确定要删除选中的 ${selectedIds.value.length} 条投诉吗?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+
+	if (status !== 'confirm') {
+		return
+	}
+
+	const result = await complaints
+		.del(selectedIds.value)
+		.then(() => true)
+		.catch(() => false)
+
+	if (result) {
+		ElMessage.success('删除成功')
+		await getComplaintList()
+	}
+}
+
+// 初始化
+onMounted(() => {
+	getComplaintList()
+})
+
+type SimpleUser = {
+	id: number
+	userNickname: string
+}
+
+//用户获取
+const {
+	state: userList,
+	isLoading: isLoadingUserList,
+	execute: loadingUserList,
+} = useAsyncState<SimpleUser[]>(async (name: string) => {
+	//为了防止默认情况下的用户列表不存在已经分配的用户,需要提前获取这个用户的bean。
+	const user_id = editForm.value?.assignee ?? undefined
+
+	const [origin_user, data]: [origin_user: SimpleUser | undefined, data: SimpleUser[]] = await Promise.all([
+		user_id === undefined ? Promise.resolve(undefined) : system.user.detail(user_id).catch(() => undefined),
+		system.user
+			.getList({ keyWords: name, status: 1 })
+			.then((res: { list: SimpleUser[] }) => res.list)
+			.catch(() => []),
+	])
+
+	if (data.length === 0) {
+		return origin_user !== undefined ? [origin_user] : []
+	}
+
+	if (data.filter((item) => item.id === origin_user?.id).length !== 0) {
+		return data
+	}
+
+	if (origin_user !== undefined) {
+		return [origin_user, ...data]
+	} else {
+		return data
+	}
+}, [])
+
+const feedback = ref(false)
+const feedFormRef = ref()
+const feedCreateForm = ref<Omit<FeedbackCreateParams, 'surveyCode'>>({
+	contactInfo: '',
+	investigatorName: '',
+	ticketNo: 0,
+	processingSpeed: '',
+	staffAttitude: '',
+	resolutionEffect: '',
+	otherSuggestions: '',
+})
+
+// 反馈表单验证规则
+const feedFormRules = {
+	surveyCode: [{ required: true, message: '请输入问卷编号', trigger: 'blur' }],
+	investigatorName: [{ required: true, message: '请输入调查者姓名', trigger: 'blur' }],
+	contactInfo: [
+		{ required: true, message: '请输入联系信息', trigger: 'blur' },
+		{ pattern: /^(1[3-9]\d{9}|[\w.-]+@[\w.-]+\.\w+)$/, message: '请输入正确的手机号或邮箱', trigger: 'blur' },
+	],
+	processingSpeed: [{ required: true, message: '请选择处理速度评价', trigger: 'change' }],
+	staffAttitude: [{ required: true, message: '请选择工作人员态度评价', trigger: 'change' }],
+	resolutionEffect: [{ required: true, message: '请选择解决效果评价', trigger: 'change' }],
+}
+
+//投诉等级,投诉来源,投诉类型
+const {
+	related_level,
+}: {
+	[key: string]: Array<{
+		label: string
+		value: string
+	}>
+} = proxy.useDict('related_level')
+
+const handleFeedback = (row: Complaint) => {
+	// 重置表单
+	feedFormRef.value?.resetFields()
+	// 设置投诉编号(不可修改)
+	feedCreateForm.value.ticketNo = row.id
+	// 清空其他字段
+	feedCreateForm.value = {
+		...feedCreateForm.value,
+		contactInfo: '',
+		investigatorName: '',
+		processingSpeed: '',
+		staffAttitude: '',
+		resolutionEffect: '',
+		otherSuggestions: '',
+	}
+	feedback.value = true
+}
+
+// 取消反馈
+const handleFeedbackCancel = () => {
+	feedback.value = false
+	feedFormRef.value?.resetFields()
+}
+
+const { loading: createFeedbackLoading, doLoading: createFeedback } = useLoading(async () => {
+	const valid = await feedFormRef.value?.validate().catch(() => false)
+	if (!valid) {
+		return
+	}
+
+	const result = await feedback_api
+		.create({ surveyCode: `${new Date().getFullYear()}-${new Date().getMonth()}-${new Date().getDay()}-${(Math.random() * 100000).toFixed(0)}`, ...feedCreateForm.value })
+		.then(() => true)
+		.catch(() => false)
+
+	if (result) {
+		ElMessage.success('反馈成功')
+	}
+	handleFeedbackCancel()
+})
+
+const complaintDetailId = ref<number | undefined>(undefined)
+const showDetail = ref(false)
+const handleDetail = (row: Complaint) => {
+	complaintDetailId.value = row.id
+	showDetail.value = true
+}
+
+</script>
+
+<template>
+	<div class="page">
+		<el-card shadow="never">
+			<!-- 搜索和筛选区域 -->
+			<el-form :model="queryParams" ref="queryRef" inline class="mb-4">
+				<el-form-item>
+					<el-input
+						v-model="queryParams.name"
+						placeholder="输入名称搜索设备"
+						clearable
+						style="width: 300px"
+						:prefix-icon="Search"
+						@keyup.enter="handleSearch"
+					/>
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="queryParams.status" placeholder="状态" style="width: 120px">
+						<el-option label="全部" value="" />
+						<el-option label="待处理" value="pending" />
+						<el-option label="处理中" value="processing" />
+						<el-option label="已完成" value="completed" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="queryParams.category" placeholder="分类" style="width: 120px">
+						<el-option label="全部" value="" />
+						<el-option v-for="i in report_type" :label="i.label" :value="i.value" :key="i.value" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-select v-model="queryParams.level" placeholder="等级" style="width: 120px">
+						<el-option label="全部" value="" />
+						<el-option v-for="i in report_level" :label="i.label" :value="i.value" :key="i.value"></el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="handleSearch">
+						<el-icon>
+							<Search />
+						</el-icon>
+						查询
+					</el-button>
+				</el-form-item>
+			</el-form>
+
+			<!-- 操作按钮区域 -->
+			<div class="flex justify-between items-center mb-4">
+				<div class="flex gap-2">
+					<el-button type="primary" @click="handleAdd">
+						<el-icon>
+							<Plus />
+						</el-icon>
+						新增投诉
+					</el-button>
+					<el-button @click="handleDelete" :disabled="selectedIds.length === 0">
+						<el-icon>
+							<Delete />
+						</el-icon>
+						删除
+					</el-button>
+				</div>
+			</div>
+
+			<!-- 表格 -->
+			<el-table :data="tableData" v-loading="loading" @selection-change="handleSelectionChange" style="width: 100%">
+				<el-table-column type="selection" width="55" align="center" />
+				<el-table-column prop="id" label="标识" width="160" align="center"/>
+				<el-table-column prop="title" label="投诉标题" min-width="200" show-overflow-tooltip />
+				<el-table-column prop="category" label="投诉类型" width="120" align="center">
+					<template #default="{ row }: { row: Complaint }">
+						<span>{{ formatReportType(row.category) }}</span>
+					</template>
+				</el-table-column>
+				<el-table-column prop="area" label="区域" width="80" align="center" />
+				<el-table-column prop="level" label="等级" width="80" align="center">
+					<template #default="{ row }: { row: Complaint }">
+						<span>{{ formatReportLevel(row.level) }}</span>
+					</template>
+				</el-table-column>
+				<el-table-column prop="status" label="状态" width="100" align="center">
+					<template #default="{ row }: { row: Complaint }">
+						<el-tag :type="formatReportStatusTagType(row.status)">{{ formatReportStatus(row.status) }}</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column prop="updatedAt" label="最后更新时间" width="180" align="center" />
+				<el-table-column prop="assignee" label="分配给" width="120" align="center" />
+				<el-table-column label="操作" width="300" align="center" fixed="right">
+					<template #default="{ row }: { row: Complaint }">
+						<el-button
+							size="small"
+							type="primary"
+							link
+							@click="handleDetail(row)"
+						>
+							<el-icon>
+								<View />
+							</el-icon>
+							详情
+						</el-button>
+						<el-button size="small" type="warning" link @click="handleEdit(row.id)" :loading="row.id == currentLoadingEdit && loadingEdit">
+							<el-icon>
+								<Edit />
+							</el-icon>
+							修改
+						</el-button>
+						<el-button size="small" type="danger" link @click="handleDeleteSingle(row)">
+							<el-icon>
+								<Delete />
+							</el-icon>
+							删除
+						</el-button>
+
+						<el-button size="small" type="info" link @click="handleFeedback(row)">
+							<el-icon>
+								<Search />
+							</el-icon>
+							反馈
+						</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<pagination
+				v-show="total > 0"
+				:total="total"
+				v-model:page="queryParams.pageNum"
+				v-model:limit="queryParams.pageSize"
+				@pagination="handlePageChange"
+			/>
+		</el-card>
+
+		<!-- 新增投诉对话框 -->
+		<el-dialog v-model="addDialogVisible" title="新建投诉" width="800px" :close-on-click-modal="false">
+			<el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-width="100px" label-position="left">
+				<el-row :gutter="35">
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" >
+						<el-form-item label="投诉标题" prop="title" required>
+							<el-input v-model="addForm.title" placeholder="请输入投诉标题" maxlength="100" show-word-limit />
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉类型" prop="category" required>
+							<el-select v-model="addForm.category" placeholder="选择投诉类型" style="width: 100%">
+								<el-option v-for="item in report_type" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉来源" prop="source" required>
+							<el-select v-model="addForm.source" placeholder="选择投诉来源" style="width: 100%">
+								<el-option v-for="item in report_source" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉区域" prop="area" required>
+							<el-select v-model="addForm.area" placeholder="选择投诉区域" style="width: 100%">
+								<el-option label="A区" value="A区" />
+								<el-option label="B区" value="B区" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉人姓名" prop="complainantName" required>
+							<el-input v-model="addForm.complainantName" placeholder="请输入投诉人姓名" maxlength="50" />
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="联系方式" prop="contact">
+							<el-input v-model="addForm.contact" placeholder="请输入联系电话或邮箱" maxlength="50" />
+						</el-form-item>
+					</el-col>
+				</el-row>
+				<el-form-item label="投诉等级" prop="level" required>
+					<el-radio-group v-model="addForm.level">
+						<el-radio v-for="item in report_level" :label="item.value" :key="item.value">
+							<span>{{ item.label }}</span>
+						</el-radio>
+					</el-radio-group>
+				</el-form-item>
+
+				<el-form-item label="投诉内容" prop="content" required>
+					<el-input v-model="addForm.content" type="textarea" :rows="4" placeholder="请详细描述投诉内容..." maxlength="500" show-word-limit />
+				</el-form-item>
+
+				<el-form-item label="指派负责人" prop="assignee">
+					<el-select
+						v-model="addForm.assignee"
+						placeholder="选择负责人"
+						style="width: 100%"
+						filterable
+						remote
+						:remote-method="(data: string) => loadingUserList(100,data)"
+						:loading="isLoadingUserList"
+						clearable
+					>
+						<el-option v-for="user in userList" :key="user.id" :label="user.userNickname" :value="user.id" />
+					</el-select>
+				</el-form-item>
+			</el-form>
+
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="handleAddCancel">取消</el-button>
+					<el-button type="primary" @click="handleAddConfirm">
+						提交投诉
+					</el-button>
+					<el-button @click="handleDemoAdded">保存草稿</el-button>
+				</div>
+			</template>
+		</el-dialog>
+
+		<!-- 编辑投诉对话框 -->
+		<el-dialog v-model="editDialogVisible" title="编辑投诉" width="700px" :close-on-click-modal="false">
+			<el-form ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="100px" label-position="left">
+				<el-row :gutter="35">
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" >
+						<el-form-item label="投诉标题" prop="title" required>
+							<el-input v-model="editForm.title" placeholder="请输入投诉标题" maxlength="100" show-word-limit />
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉类型" prop="category" required>
+							<el-select v-model="editForm.category" placeholder="选择投诉类型" style="width: 100%">
+								<el-option v-for="item in report_type" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉来源" prop="source" required>
+							<el-select v-model="editForm.source" placeholder="选择投诉来源" style="width: 100%">
+								<el-option v-for="item in report_source" :key="item.value" :label="item.label" :value="item.value" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉区域" prop="area" required>
+							<el-select v-model="editForm.area" placeholder="选择投诉区域" style="width: 100%">
+								<el-option label="A区" value="A区" />
+								<el-option label="B区" value="B区" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="投诉人姓名" prop="complainantName" required>
+							<el-input v-model="editForm.complainantName" placeholder="请输入投诉人姓名" maxlength="50" />
+						</el-form-item>
+					</el-col>
+
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="联系方式" prop="contact">
+							<el-input v-model="editForm.contact" placeholder="请输入联系电话或邮箱" maxlength="50" />
+						</el-form-item>
+					</el-col>
+				</el-row>
+
+				<el-form-item label="投诉等级" prop="level" required>
+					<el-radio-group v-model="editForm.level">
+						<el-radio v-for="item in report_level" :label="item.value" :key="item.value">
+							<span>{{ item.label }}</span>
+						</el-radio>
+					</el-radio-group>
+				</el-form-item>
+
+				<el-form-item label="投诉内容" prop="content" required>
+					<el-input v-model="editForm.content" type="textarea" :rows="4" placeholder="请详细描述投诉内容..." maxlength="500" show-word-limit />
+				</el-form-item>
+				<el-form-item label="指派负责人" prop="assignee">
+					<el-select
+						v-model="editForm.assignee"
+						placeholder="选择负责人"
+						style="width: 100%"
+						filterable
+						remote
+						:remote-method="(data: string) => loadingUserList(100,data)"
+						:loading="isLoadingUserList"
+						clearable
+					>
+						<el-option v-for="user in userList" :key="user.id" :label="user.userNickname" :value="user.id" />
+					</el-select>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="handleEditCancel">取消</el-button>
+					<el-button type="primary" @click="handleEditConfirm">
+						提交修改
+					</el-button>
+					<el-button @click="handleEditCancel">保存草稿</el-button>
+				</div>
+			</template>
+		</el-dialog>
+
+		<!-- 反馈对话框 -->
+		<el-dialog v-model="feedback" title="投诉反馈" width="700px" :close-on-click-modal="false">
+			<el-form ref="feedFormRef" :model="feedCreateForm" :rules="feedFormRules" label-width="120px" label-position="left">
+<!--				<el-form-item label="问卷编号" prop="surveyCode" required>-->
+<!--					<el-input v-model="feedCreateForm.surveyCode" placeholder="请输入问卷编号" maxlength="50" />-->
+<!--				</el-form-item>-->
+
+<!--				<el-form-item label="投诉编号" prop="ticketNo">-->
+<!--					<el-input v-model="feedCreateForm.ticketNo" placeholder="投诉编号" disabled />-->
+<!--				</el-form-item>-->
+				<div style="display: flex">
+					<div style="flex: 1">
+						<el-form-item label="调查者姓名" prop="investigatorName" required>
+							<el-input v-model="feedCreateForm.investigatorName" placeholder="请输入调查者姓名" maxlength="50" />
+						</el-form-item>
+					</div>
+					<div style="width: 32px"></div>
+					<div style="flex: 1">
+						<el-form-item label="联系信息" prop="contactInfo" required>
+							<el-input v-model="feedCreateForm.contactInfo" placeholder="请输入联系电话或邮箱" maxlength="100" />
+						</el-form-item>
+					</div>
+				</div>
+
+
+
+				<el-form-item label="处理速度" prop="processingSpeed" required>
+					<el-radio-group v-model="feedCreateForm.processingSpeed">
+						<el-radio v-for="item in related_level" :label="item.value" :key="item.value">
+							<span>{{ item.label }}</span>
+						</el-radio>
+					</el-radio-group>
+					<!--					<el-select v-model="feedCreateForm.processingSpeed" placeholder="请选择处理速度评价" style="width: 100%">-->
+					<!--						<el-option v-for="item in related_level" :key="item.value" :label="item.label" :value="item.value" />-->
+					<!--					</el-select>-->
+				</el-form-item>
+
+				<el-form-item label="工作人员态度" prop="staffAttitude" required>
+					<el-radio-group v-model="feedCreateForm.staffAttitude">
+						<el-radio v-for="item in related_level" :label="item.value" :key="item.value">
+							<span>{{ item.label }}</span>
+						</el-radio>
+					</el-radio-group>
+					<!--					<el-select v-model="feedCreateForm.staffAttitude" placeholder="请选择工作人员态度评价" style="width: 100%">-->
+					<!--						<el-option v-for="item in related_level" :key="item.value" :label="item.label" :value="item.value" />-->
+					<!--					</el-select>-->
+				</el-form-item>
+
+				<el-form-item label="解决效果" prop="resolutionEffect" required>
+					<el-radio-group v-model="feedCreateForm.resolutionEffect">
+						<el-radio v-for="item in related_level" :label="item.value" :key="item.value">
+							<span>{{ item.label }}</span>
+						</el-radio>
+					</el-radio-group>
+					<!--					<el-select v-model="feedCreateForm.resolutionEffect" placeholder="请选择解决效果评价" style="width: 100%">-->
+					<!--						<el-option v-for="item in related_level" :key="item.value" :label="item.label" :value="item.value" />-->
+					<!--					</el-select>-->
+				</el-form-item>
+
+				<el-form-item label="其他建议" prop="otherSuggestions">
+					<el-input
+						v-model="feedCreateForm.otherSuggestions"
+						type="textarea"
+						:rows="4"
+						placeholder="请输入其他建议..."
+						maxlength="500"
+						show-word-limit
+					/>
+				</el-form-item>
+			</el-form>
+
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="handleFeedbackCancel">取消</el-button>
+					<el-button type="primary" @click="createFeedback" :loading="createFeedbackLoading">
+						<el-icon>
+							<Plus />
+						</el-icon>
+						提交反馈
+					</el-button>
+				</div>
+			</template>
+		</el-dialog>
+
+		<report-detail-dialog :id="complaintDetailId" v-model:visible="showDetail"/>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.page {
+	padding: 20px;
+}
+
+.breadcrumb-container {
+	font-size: 14px;
+	color: #606266;
+}
+
+.flex {
+	display: flex;
+}
+
+.justify-between {
+	justify-content: space-between;
+}
+
+.items-center {
+	align-items: center;
+}
+
+.gap-2 > * + * {
+	margin-left: 8px;
+}
+
+.mb-4 {
+	margin-bottom: 16px;
+}
+
+.mt-4 {
+	margin-top: 16px;
+}
+
+.text-gray-500 {
+	color: #909399;
+}
+
+.text-gray-400 {
+	color: #c0c4cc;
+}
+
+.text-blue-600 {
+	color: #409eff;
+}
+
+.mx-2 {
+	margin-left: 8px;
+	margin-right: 8px;
+}
+
+.mr-2 {
+	margin-right: 8px;
+}
+
+.w-2 {
+	width: 8px;
+}
+
+.h-2 {
+	height: 8px;
+}
+
+.bg-blue-500 {
+	background-color: #409eff;
+}
+
+.rounded-full {
+	border-radius: 50%;
+}
+
+.text-sm {
+	font-size: 12px;
+}
+
+.dialog-footer {
+	display: flex;
+	justify-content: flex-end;
+	gap: 8px;
+}
+</style>

+ 936 - 0
src/views/system/report/summary/index.vue

@@ -0,0 +1,936 @@
+<script setup lang="ts">
+import { ref, onMounted, computed, getCurrentInstance, unref, watch } from 'vue'
+import {
+	Document,
+	Clock,
+	CircleCheck,
+	Warning,
+	Timer,
+	TrendCharts,
+	PieChart,
+	Position,
+	List,
+	CaretTop,
+	CaretBottom,
+} from '@element-plus/icons-vue'
+import api from '/@/api/system/report/statistics'
+import report from '/@/api/system/report/complaints'
+import { Complaint, StatisticsQueryParams } from '/@/api/system/report/type'
+
+// 按需引入 ECharts
+import * as echarts from 'echarts/core'
+import { PieChart as PieChartComponent, BarChart, LineChart } from 'echarts/charts'
+import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { useAsyncState, useEventListener } from '@vueuse/core'
+import { BarSeriesOption, ComposeOption, PieSeriesOption } from 'echarts'
+import ReportDetailDialog from '/@/views/system/report/componments/report-detail-dialog.vue'
+
+// 注册必须的组件
+echarts.use([TitleComponent, TooltipComponent, LegendComponent, GridComponent, PieChartComponent, BarChart, LineChart, LabelLayout, CanvasRenderer])
+
+const { proxy } = getCurrentInstance() as any
+
+//投诉等级,投诉来源,投诉类型
+const {
+	report_level,
+	report_source,
+	report_type,
+}: {
+	[key: string]: Array<{
+		label: string
+		value: string
+	}>
+} = proxy.useDict('report_level', 'report_source', 'report_type')
+
+// eslint-disable-next-line no-unused-vars
+const formatReportLevel = computed<(value: string) => string>(() => {
+	const levels = unref(report_level)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (levels === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(levels, value)
+	}
+})
+
+// eslint-disable-next-line no-unused-vars
+const formatReportSource = computed<(value: string) => string>(() => {
+	const sources = unref(report_source)
+	return (value: string) => {
+		if (value === undefined) {
+			return '-'
+		}
+		if (sources === undefined) {
+			return '-'
+		}
+		return proxy.selectDictLabel(sources, value)
+	}
+})
+const formatReportStatus = (value: Complaint['status']) => {
+	let a = '-'
+	switch (value) {
+		case 'completed':
+			a = '已完成'
+			break
+		case 'pending':
+			a = '待处理'
+			break
+		case 'processing':
+			a = '进行中'
+			break
+	}
+	return a
+}
+
+// 响应式数据
+// const typeDistributionData = ref<ComplaintTypeDistribution[]>([])
+// const monthlyTrendData = ref<MonthlyTrend[]>([])
+// const areaDistributionData = ref<AreaDistribution[]>([])
+
+// 统计部分
+
+//表单
+const timeRange = ref<StatisticsQueryParams['timeRange']>('month')
+watch(timeRange, (newVal: StatisticsQueryParams['timeRange']) => {
+	getStatistics(100, { timeRange: newVal })
+})
+
+//数据展示
+const satisfactionRef = ref<HTMLElement>()
+const satisfactionRefMounted = ref(false)
+let satisfactionChart: echarts.ECharts
+
+//初始化时挂载echarts
+onMounted(() => {
+	satisfactionChart = echarts.init(satisfactionRef.value!)
+	satisfactionRefMounted.value = true
+})
+
+useEventListener('resize', () => {
+	if (satisfactionChart) {
+		satisfactionChart.resize()
+	}
+})
+
+//异步获取数据
+const {
+	state: statistics,
+	execute: getStatistics,
+} = useAsyncState(async (range: StatisticsQueryParams) => api.overview(range), undefined)
+
+//通过数据和组件挂载状态决定ECOption
+const statisticsOption = computed(() => {
+	const prepare = statistics.value
+	if (prepare === undefined) {
+		//数据加载必须完成
+		return undefined
+	}
+
+	if (!satisfactionRefMounted.value) {
+		//组件必须被挂载
+		return undefined
+	}
+
+	const option: ComposeOption<PieSeriesOption> = {
+		series: [
+			{
+				type: 'pie',
+				radius: ['70%', '90%'],
+				center: ['50%', '50%'],
+				startAngle: 90,
+				// endAngle: 450,
+				data: [
+					{ value: prepare.satisfactionScore, itemStyle: { color: '#52c41a' } },
+					{ value: 5 - prepare.satisfactionScore, itemStyle: { color: '#f0f0f0' } },
+				],
+				label: {
+					show: true,
+					position: 'center',
+					formatter: () => `{value|${prepare.satisfactionScore}%}`,
+					rich: {
+						value: {
+							fontSize: 24,
+							fontWeight: 'bold',
+							color: '#52c41a',
+						},
+						unit: {
+							fontSize: 14,
+							color: '#666',
+						},
+					},
+				},
+				labelLine: { show: false },
+				silent: true,
+			},
+		],
+	}
+
+	return option
+})
+watch(statisticsOption, (newVal) => {
+	if (newVal !== undefined) {
+		satisfactionChart.setOption(newVal)
+	}
+})
+const satisfactionStars = computed(() => {
+	return Math.round((statistics.value?.satisfactionScore ?? 0) * 10) / 10
+})
+
+const typeDistributionRef = ref<HTMLElement>()
+const typeDistributionRefMounted = ref(false)
+let typeChart: echarts.ECharts
+//初始化时挂载echarts
+onMounted(() => {
+	typeChart = echarts.init(typeDistributionRef.value!)
+	typeDistributionRefMounted.value = true
+})
+
+useEventListener('resize', () => {
+	if (typeChart) {
+		typeChart.resize()
+	}
+})
+
+//异步获取数据
+const { state: typeDistributionData } = useAsyncState(async () => api.types(), undefined)
+
+//通过数据和组件挂载状态决定ECOption
+const typeDistributionOption = computed(() => {
+	const prepare = typeDistributionData.value
+	if (prepare === undefined) {
+		//数据加载必须完成
+		return undefined
+	}
+	if (!typeDistributionRefMounted.value) {
+		//组件必须被挂载
+		return undefined
+	}
+
+	const report_type_impl = unref(report_type)
+
+	const option: ComposeOption<PieSeriesOption> = {
+		tooltip: {
+			trigger: 'item',
+			formatter: '{b}: {c} ({d}%)',
+		},
+		legend: {
+			type: 'scroll',
+			orient: 'vertical',
+			right: '5%',
+			top: 'center',
+			itemGap: 20,
+			textStyle: { fontSize: 12 },
+		},
+		series: [
+			{
+				type: 'pie',
+				radius: ['0%', '60%'],
+				center: ['35%', '50%'],
+				data: prepare.map((item) => ({
+					name: proxy.selectDictLabel(report_type_impl, item.type),
+					value: item.count,
+				})),
+				label: { show: false },
+				emphasis: {
+					itemStyle: {
+						shadowBlur: 10,
+						shadowOffsetX: 0,
+						shadowColor: 'rgba(0, 0, 0, 0.5)',
+					},
+				},
+			},
+		],
+	}
+
+	return option
+})
+
+watch(typeDistributionOption, (newVal) => {
+	if (newVal !== undefined) {
+		typeChart.setOption(newVal)
+	}
+})
+
+const monthlyTrendRef = ref<HTMLElement>()
+const monthlyTrendRefMounted = ref(false)
+let monthTrendChart: echarts.ECharts
+//初始化时挂载echarts
+onMounted(() => {
+	monthTrendChart = echarts.init(monthlyTrendRef.value!)
+	monthlyTrendRefMounted.value = true
+})
+useEventListener('resize', () => {
+	if (monthTrendChart) {
+		monthTrendChart.resize()
+	}
+})
+//异步获取数据
+const { state: monthlyTrendData } = useAsyncState(async () => api.monthlyTrends(), undefined)
+//通过数据和组件挂载状态决定ECOption
+const monthlyTrendOption = computed(() => {
+	const prepare = monthlyTrendData.value
+	if (prepare === undefined) {
+		//数据加载必须完成
+		return undefined
+	}
+	if (!monthlyTrendRefMounted.value) {
+		//组件必须被挂载
+		return undefined
+	}
+	const option: ComposeOption<BarSeriesOption> = {
+		tooltip: {
+			trigger: 'axis',
+			formatter: (params: any) => {
+				const data = params[0]
+				return `${data.name}<br/>完成率: ${data.value}%`
+			},
+		},
+		xAxis: {
+			type: 'category',
+			data: prepare.map((item) => item.month),
+			axisLine: { show: false },
+			axisTick: { show: false },
+		},
+		yAxis: {
+			type: 'value',
+			min: 85,
+			max: 100,
+			axisLine: { show: false },
+			axisTick: { show: false },
+			splitLine: {
+				lineStyle: {
+					color: '#f0f0f0',
+					type: 'dashed',
+				},
+			},
+		},
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			top: '3%',
+			containLabel: true,
+		},
+		series: [
+			{
+				type: 'bar',
+				data: prepare.map((item) => item.completionRate),
+				itemStyle: {
+					color: '#000',
+					borderRadius: [2, 2, 0, 0],
+				},
+				barWidth: '60%',
+			},
+		],
+	}
+	return option
+})
+
+watch(monthlyTrendOption, (newVal) => {
+	if (newVal !== undefined) {
+		monthTrendChart.setOption(newVal)
+	}
+})
+
+useEventListener('resize', () => {
+	if (monthTrendChart) {
+		monthTrendChart.resize()
+	}
+})
+
+const areaDistributionRef = ref<HTMLElement>()
+const areaDistributionRefMounted = ref(false)
+let areaChart: echarts.ECharts
+//初始化时挂载echarts
+onMounted(() => {
+	areaChart = echarts.init(areaDistributionRef.value!)
+	areaDistributionRefMounted.value = true
+})
+useEventListener('resize', () => {
+	if (areaChart) {
+		areaChart.resize()
+	}
+})
+//异步获取数据
+const { state: areaDistributionData } = useAsyncState(async () => api.areas(), undefined)
+//通过数据和组件挂载状态决定ECOption
+const areaDistributionOption = computed(() => {
+	const prepare = areaDistributionData.value
+	if (prepare === undefined) {
+		//数据加载必须完成
+		return undefined
+	}
+	if (!areaDistributionRefMounted.value) {
+		//组件必须被挂载
+		return undefined
+	}
+
+	const option: ComposeOption<BarSeriesOption> = {
+		tooltip: {
+			trigger: 'axis',
+			axisPointer: { type: 'shadow' },
+		},
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			top: '3%',
+			containLabel: true,
+		},
+		xAxis: {
+			type: 'value',
+			axisLine: { show: false },
+			axisTick: { show: false },
+			splitLine: {
+				lineStyle: {
+					color: '#f0f0f0',
+					type: 'dashed',
+				},
+			},
+		},
+		yAxis: {
+			type: 'category',
+			data: prepare.map(item => item.area),
+			axisLine: { show: false },
+			axisTick: { show: false },
+		},
+		series: [{
+			type: 'bar',
+			data: prepare.map(item => ({
+				value: item.count,
+				itemStyle: { color: '#666' },
+			})),
+			barWidth: '50%',
+			itemStyle: {
+				borderRadius: [0, 2, 2, 0],
+			},
+		}],
+	}
+	return option
+})
+watch(areaDistributionOption, (newVal) => {
+	if (newVal!== undefined) {
+		areaChart.setOption(newVal)
+	}
+})
+
+const {state: recentComplaints} = useAsyncState<Complaint[]>(async () => report.getList({orderBy: 'desc',pageSize: 5}).then((res: {list: Complaint[]})=>res.list),[])
+
+
+const detailVisible = ref(false)
+const detailComplaint = ref<number|undefined>(undefined)
+const handleDetail = (complaint: Complaint) => {
+	detailComplaint.value = complaint.id
+	detailVisible.value = true
+}
+
+const openAll = () => {
+	proxy.$router.push('/system/report/list')
+}
+</script>
+
+<template>
+	<div class="page">
+		<el-card shadow="never">
+			<!-- 标题和时间选择器 -->
+			<div class="flex justify-between flex-direction-end mb-6">
+				<el-select v-model="timeRange" placeholder="本月" style="width: 120px">
+					<el-option label="本周" value="week" />
+					<el-option label="本月" value="month" />
+					<el-option label="本季度" value="quarter" />
+					<el-option label="本年" value="year" />
+				</el-select>
+			</div>
+
+			<!-- 统计卡片区域 -->
+			<el-row :gutter="16" class="mb-6">
+				<!-- 满意度评分 - 占满一列 -->
+				<el-col :xs="24" :sm="24" :md="6" :lg="6" :xl="6" class="satisfaction-col">
+					<div class="stat-card satisfaction-card">
+						<div class="satisfaction-circle" ref="satisfactionRef"></div>
+						<div class="satisfaction-info">
+							<div class="satisfaction-title">整体满意度</div>
+							<div class="satisfaction-subtitle">基于 {{ statistics?.satisfactionTotal ?? 0 }} 份反馈</div>
+							<div class="satisfaction-stars">
+								<el-rate v-model="satisfactionStars" disabled show-score text-color="#ff9900" score-template="{value}" :max="5" />
+							</div>
+							<div class="satisfaction-trend">较上月 +0.3</div>
+						</div>
+					</div>
+				</el-col>
+
+				<!-- 其他统计卡片 -->
+				<el-col :xs="24" :sm="24" :md="18" :lg="18" :xl="18">
+					<el-row :gutter="16" class="stat-cards-container">
+						<!-- 总投诉 -->
+						<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
+							<div class="stat-card">
+								<div class="stat-icon">
+									<el-icon size="24" color="#409eff">
+										<Document />
+									</el-icon>
+								</div>
+								<div class="stat-content">
+									<div class="stat-label">总投诉</div>
+									<div class="stat-value">{{ statistics?.totalComplaints?.toLocaleString() ?? '-' }}</div>
+									<div class="stat-trend positive">
+										<el-icon>
+											<CaretTop />
+										</el-icon>
+										+12%
+									</div>
+								</div>
+							</div>
+						</el-col>
+
+						<!-- 待处理 -->
+						<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
+							<div class="stat-card">
+								<div class="stat-icon">
+									<el-icon size="24" color="#e6a23c">
+										<Clock />
+									</el-icon>
+								</div>
+								<div class="stat-content">
+									<div class="stat-label">待处理</div>
+									<div class="stat-value">{{ statistics?.pendingComplaints ?? '-' }}</div>
+									<div class="stat-trend negative">
+										<el-icon>
+											<CaretBottom />
+										</el-icon>
+										-5%
+									</div>
+								</div>
+							</div>
+						</el-col>
+
+						<!-- 已完成 -->
+						<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
+							<div class="stat-card">
+								<div class="stat-icon">
+									<el-icon size="24" color="#67c23a">
+										<CircleCheck />
+									</el-icon>
+								</div>
+								<div class="stat-content">
+									<div class="stat-label">已完成</div>
+									<div class="stat-value">{{ statistics?.completedComplaints?.toLocaleString() ?? '-' }}</div>
+									<div class="stat-trend positive">
+										<el-icon>
+											<CaretTop />
+										</el-icon>
+										+18%
+									</div>
+								</div>
+							</div>
+						</el-col>
+
+						<!-- 紧急 -->
+						<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
+							<div class="stat-card">
+								<div class="stat-icon">
+									<el-icon size="24" color="#f56c6c">
+										<Warning />
+									</el-icon>
+								</div>
+								<div class="stat-content">
+									<div class="stat-label">紧急</div>
+									<div class="stat-value">{{ statistics?.urgentComplaints ?? '-' }}</div>
+									<div class="stat-trend positive">
+										<el-icon>
+											<CaretTop />
+										</el-icon>
+										+3%
+									</div>
+								</div>
+							</div>
+						</el-col>
+
+						<!-- 处理时间 -->
+						<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
+							<div class="stat-card">
+								<div class="stat-icon">
+									<el-icon size="24" color="#909399">
+										<Timer />
+									</el-icon>
+								</div>
+								<div class="stat-content">
+									<div class="stat-label">处理时间</div>
+									<div class="stat-value">{{ statistics?.averageProcessingTime ?? '-' }}天</div>
+									<div class="stat-trend negative">
+										<el-icon>
+											<CaretBottom />
+										</el-icon>
+										-0.5天
+									</div>
+								</div>
+							</div>
+						</el-col>
+
+						<!-- 完成率 -->
+						<el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
+							<div class="stat-card">
+								<div class="stat-icon">
+									<el-icon size="24" color="#409eff">
+										<TrendCharts />
+									</el-icon>
+								</div>
+								<div class="stat-content">
+									<div class="stat-label">完成率</div>
+									<div class="stat-value">{{ statistics?.completionRate ?? '-' }}%</div>
+									<div class="stat-trend positive">
+										<el-icon>
+											<CaretTop />
+										</el-icon>
+										+2.1%
+									</div>
+								</div>
+							</div>
+						</el-col>
+					</el-row>
+				</el-col>
+			</el-row>
+
+			<!-- 图表区域 -->
+			<el-row :gutter="16" class="mb-6">
+				<!-- 投诉类型分布 -->
+				<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+					<div class="chart-card">
+						<div class="chart-title">
+							<el-icon>
+								<PieChart />
+							</el-icon>
+							投诉类型分布
+						</div>
+						<div class="chart-container" ref="typeDistributionRef"></div>
+					</div>
+				</el-col>
+
+				<!-- 月度完成率趋势 -->
+				<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+					<div class="chart-card">
+						<div class="chart-title">
+							<el-icon>
+								<TrendCharts />
+							</el-icon>
+							月度完成率趋势
+						</div>
+						<div class="chart-container" ref="monthlyTrendRef"></div>
+					</div>
+				</el-col>
+
+				<!-- 区域分布 -->
+				<el-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+					<div class="chart-card">
+						<div class="chart-title">
+							<el-icon>
+								<Position />
+							</el-icon>
+							区域分布
+						</div>
+						<div class="chart-container" ref="areaDistributionRef"></div>
+					</div>
+				</el-col>
+			</el-row>
+
+			<!-- 最近投诉动态 -->
+			<div class="recent-complaints">
+				<div class="flex justify-between items-center mb-4">
+					<div class="section-title">
+						<el-icon>
+							<List />
+						</el-icon>
+						最近投诉动态
+					</div>
+					<el-button type="primary" link @click="openAll">查看全部</el-button>
+				</div>
+
+				<div class="complaint-list">
+					<div v-for="complaint in recentComplaints" :key="complaint.id" class="complaint-item">
+						<div class="complaint-id">
+							<span class="id-text">#{{ complaint.id }}</span>
+							<el-tag size="small">
+								{{ formatReportLevel(complaint.level) }}
+							</el-tag>
+						</div>
+						<div class="complaint-title">{{ complaint.title }}</div>
+						<div class="complaint-meta">
+							<span class="complaint-time">{{ complaint.createdAt }}</span>
+							<span class="complaint-assignee">{{ complaint.assignee }}</span>
+							<div class="complaint-actions">
+								<el-tag size="small">
+									{{ formatReportStatus(complaint.status) }}
+								</el-tag>
+								<el-button type="primary" link size="small" @click="handleDetail(complaint)">详情</el-button>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+		</el-card>
+
+		<report-detail-dialog :id="detailComplaint" v-model:visible="detailVisible"/>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.flex-direction-end {
+	flex-direction: row-reverse;
+}
+.page {
+	padding: 20px;
+}
+
+.breadcrumb-container {
+	font-size: 14px;
+	color: #606266;
+}
+
+.text-xl {
+	font-size: 1.25rem;
+}
+
+.font-semibold {
+	font-weight: 600;
+}
+
+.stat-card {
+	background: white;
+	border-radius: 8px;
+	padding: 20px;
+	height: 140px;
+	border: 1px solid #e4e7ed;
+	display: flex;
+	align-items: center;
+	gap: 16px;
+	margin-bottom: 16px;
+
+	.stat-icon {
+		width: 48px;
+		height: 48px;
+		border-radius: 8px;
+		background: #f5f7fa;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.stat-content {
+		flex: 1;
+
+		.stat-label {
+			font-size: 14px;
+			color: #606266;
+			margin-bottom: 8px;
+		}
+
+		.stat-value {
+			font-size: 24px;
+			font-weight: bold;
+			color: #303133;
+			margin-bottom: 4px;
+		}
+
+		.stat-trend {
+			font-size: 12px;
+			display: flex;
+			align-items: center;
+			gap: 2px;
+
+			&.positive {
+				color: #67c23a;
+			}
+
+			&.negative {
+				color: #f56c6c;
+			}
+		}
+	}
+}
+
+.satisfaction-card {
+	flex-direction: column;
+	height: 300px;
+	justify-content: center;
+	margin-bottom: 0;
+
+	.satisfaction-circle {
+		width: 120px;
+		height: 120px;
+		margin-bottom: 16px;
+	}
+
+	.satisfaction-info {
+		text-align: center;
+
+		.satisfaction-title {
+			font-size: 14px;
+			font-weight: bold;
+			color: #303133;
+			margin-bottom: 4px;
+		}
+
+		.satisfaction-subtitle {
+			font-size: 12px;
+			color: #909399;
+			margin-bottom: 8px;
+		}
+
+		.satisfaction-stars {
+			margin-bottom: 8px;
+		}
+
+		.satisfaction-trend {
+			font-size: 12px;
+			color: #67c23a;
+		}
+	}
+}
+
+.chart-card {
+	background: white;
+	border-radius: 8px;
+	padding: 20px;
+	height: 320px;
+	border: 1px solid #e4e7ed;
+
+	.chart-title {
+		font-size: 16px;
+		font-weight: bold;
+		color: #303133;
+		margin-bottom: 16px;
+		display: flex;
+		align-items: center;
+		gap: 8px;
+	}
+
+	.chart-container {
+		height: 260px;
+	}
+}
+
+.recent-complaints {
+	background: white;
+	border-radius: 8px;
+	padding: 20px;
+	border: 1px solid #e4e7ed;
+
+	.section-title {
+		font-size: 16px;
+		font-weight: bold;
+		color: #303133;
+		display: flex;
+		align-items: center;
+		gap: 8px;
+	}
+
+	.complaint-list {
+		.complaint-item {
+			border-bottom: 1px solid #f0f0f0;
+			padding: 16px 0;
+
+			&:last-child {
+				border-bottom: none;
+			}
+
+			.complaint-id {
+				display: flex;
+				align-items: center;
+				gap: 8px;
+				margin-bottom: 8px;
+
+				.id-text {
+					color: #409eff;
+					font-weight: bold;
+				}
+			}
+
+			.complaint-title {
+				font-size: 14px;
+				color: #303133;
+				margin-bottom: 8px;
+			}
+
+			.complaint-meta {
+				display: flex;
+				align-items: center;
+				justify-content: space-between;
+				font-size: 12px;
+				color: #909399;
+
+				.complaint-actions {
+					display: flex;
+					align-items: center;
+					gap: 8px;
+				}
+			}
+		}
+	}
+}
+
+// 通用样式
+.flex {
+	display: flex;
+}
+
+.justify-between {
+	justify-content: space-between;
+}
+
+.items-end {
+	align-items: end;
+}
+
+.mb-4 {
+	margin-bottom: 16px;
+}
+
+.mb-6 {
+	margin-bottom: 24px;
+}
+
+.text-gray-500 {
+	color: #909399;
+}
+
+.text-gray-400 {
+	color: #c0c4cc;
+}
+
+.mx-2 {
+	margin-left: 8px;
+	margin-right: 8px;
+}
+
+// 满意度卡片列的间距控制
+.satisfaction-col {
+	@media (max-width: 767px) {
+		margin-bottom: 16px;
+	}
+}
+
+// 为统计卡片行添加间距
+.stat-cards-container {
+	.el-col {
+		margin-bottom: 0;
+
+		@media (max-width: 767px) {
+			margin-bottom: 16px;
+		}
+	}
+
+	// 第二行卡片(紧急、处理时间、完成率)添加上边距
+	.el-col:nth-child(n + 4) {
+		@media (min-width: 768px) {
+			.stat-card {
+				margin-top: 5px;
+			}
+		}
+	}
+}
+</style>