index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <template>
  2. <div class="message-manage-container">
  3. <!-- 搜索栏 -->
  4. <el-card class="search-card" shadow="never">
  5. <el-form :inline="true" :model="queryParams" class="search-form">
  6. <el-form-item label="标题">
  7. <el-input v-model="queryParams.title" placeholder="请输入标题" clearable style="width: 200px" />
  8. </el-form-item>
  9. <el-form-item label="类型">
  10. <ext-d-select v-model="queryParams.type" type="message_type" placeholder="请选择" clearable style="width: 150px" />
  11. </el-form-item>
  12. <el-form-item label="状态">
  13. <ext-d-select v-model="queryParams.status" type="message_status" placeholder="请选择" clearable style="width: 150px" />
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type="primary" @click="handleQuery">
  17. <el-icon><ele-Search /></el-icon> 查询
  18. </el-button>
  19. <el-button @click="resetQuery">
  20. <el-icon><ele-Refresh /></el-icon> 重置
  21. </el-button>
  22. </el-form-item>
  23. </el-form>
  24. </el-card>
  25. <!-- 操作栏 -->
  26. <el-card class="table-card" shadow="never">
  27. <template #header>
  28. <div class="card-header">
  29. <span>消息列表</span>
  30. <div>
  31. <el-button type="primary" @click="handleSendMessage">
  32. <el-icon><ele-Message /></el-icon> 发送消息
  33. </el-button>
  34. <el-button type="success" @click="handleBatchRead" :disabled="selectedIds.length === 0">
  35. <el-icon><ele-Check /></el-icon> 批量已读
  36. </el-button>
  37. <el-button type="danger" @click="handleBatchDelete" :disabled="selectedIds.length === 0">
  38. <el-icon><ele-Delete /></el-icon> 批量删除
  39. </el-button>
  40. </div>
  41. </div>
  42. </template>
  43. <!-- 数据表格 -->
  44. <el-table v-loading="loading" :data="tableData" border stripe @selection-change="handleSelectionChange">
  45. <el-table-column type="selection" width="50" align="center" />
  46. <el-table-column prop="id" label="ID" width="80" align="center" />
  47. <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
  48. <el-table-column prop="type" label="类型" width="120" align="center">
  49. <template #default="{ row }">
  50. <el-tag :type="getTypeColor(row.type)">{{ getTypeLabel(row.type) }}</el-tag>
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="senderName" label="发送者" width="100" align="center" />
  54. <el-table-column prop="receiverName" label="接收者" width="100" align="center" />
  55. <el-table-column prop="priority" label="优先级" width="100" align="center">
  56. <template #default="{ row }">
  57. <el-tag v-if="row.priority > 0" :type="getPriorityColor(row.priority)">
  58. {{ getPriorityLabel(row.priority) }}
  59. </el-tag>
  60. <span v-else>{{ u.fmt.fmtDict(row.priority, 'priority') }}</span>
  61. </template>
  62. </el-table-column>
  63. <el-table-column prop="status" label="状态" width="80" align="center">
  64. <template #default="{ row }">
  65. <el-tag :type="u.fmt.fmtDictColor(row.status, 'message_status')">
  66. {{ u.fmt.fmtDict(row.status, 'message_status') }}
  67. </el-tag>
  68. </template>
  69. </el-table-column>
  70. <el-table-column prop="createTime" label="发送时间" width="170" align="center" />
  71. <el-table-column label="操作" width="150" align="center" fixed="right">
  72. <template #default="{ row }">
  73. <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
  74. <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
  75. </template>
  76. </el-table-column>
  77. </el-table>
  78. <!-- 分页 -->
  79. <div class="pagination-container">
  80. <el-pagination
  81. v-model:current-page="queryParams.pageNum"
  82. v-model:page-size="queryParams.pageSize"
  83. :page-sizes="[10, 20, 50, 100]"
  84. :total="total"
  85. layout="total, sizes, prev, pager, next, jumper"
  86. @size-change="handleQuery"
  87. @current-change="handleQuery"
  88. />
  89. </div>
  90. </el-card>
  91. <!-- 发送消息对话框 -->
  92. <el-dialog v-model="sendDialogVisible" title="发送消息" width="650px" destroy-on-close>
  93. <el-form ref="sendFormRef" :model="sendForm" :rules="sendRules" label-width="100px">
  94. <el-form-item label="消息标题" prop="title">
  95. <el-input v-model="sendForm.title" placeholder="请输入消息标题" />
  96. </el-form-item>
  97. <el-row :gutter="20">
  98. <el-col :span="12">
  99. <el-form-item label="消息类型" prop="type">
  100. <ext-d-select v-model="sendForm.type" type="message_type" placeholder="请选择" style="width: 100%" />
  101. </el-form-item>
  102. </el-col>
  103. <el-col :span="12">
  104. <el-form-item label="优先级" prop="priority">
  105. <ext-d-select v-model="sendForm.priority" type="priority" placeholder="请选择" style="width: 100%" />
  106. </el-form-item>
  107. </el-col>
  108. </el-row>
  109. <el-form-item label="发送方式" prop="sendType">
  110. <el-radio-group v-model="sendForm.sendType">
  111. <el-radio value="all">全部用户</el-radio>
  112. <el-radio value="selected">指定用户</el-radio>
  113. </el-radio-group>
  114. </el-form-item>
  115. <el-form-item v-if="sendForm.sendType === 'selected'" label="选择用户" prop="receiverIds">
  116. <el-select
  117. v-model="sendForm.receiverIds"
  118. multiple
  119. filterable
  120. remote
  121. reserve-keyword
  122. placeholder="请输入用户名搜索"
  123. :remote-method="searchUsers"
  124. :loading="userLoading"
  125. style="width: 100%"
  126. >
  127. <el-option
  128. v-for="user in userList"
  129. :key="user.id"
  130. :label="`${user.nickname}(${user.username})`"
  131. :value="user.id"
  132. />
  133. </el-select>
  134. <div class="user-tip">已选择 {{ sendForm.receiverIds.length }} 个用户</div>
  135. </el-form-item>
  136. <el-form-item label="消息内容" prop="content">
  137. <el-input v-model="sendForm.content" type="textarea" :rows="5" placeholder="请输入消息内容" />
  138. </el-form-item>
  139. </el-form>
  140. <template #footer>
  141. <el-button @click="sendDialogVisible = false">取消</el-button>
  142. <el-button type="primary" @click="handleSendSubmit" :loading="sendLoading">发送</el-button>
  143. </template>
  144. </el-dialog>
  145. <!-- 查看消息对话框 -->
  146. <el-dialog v-model="viewDialogVisible" title="消息详情" width="500px">
  147. <div class="message-detail" v-if="currentMessage">
  148. <div class="detail-row">
  149. <span class="label">标题:</span>
  150. <span>{{ currentMessage.title }}</span>
  151. </div>
  152. <div class="detail-row">
  153. <span class="label">类型:</span>
  154. <el-tag :type="getTypeColor(currentMessage.type)">{{ getTypeLabel(currentMessage.type) }}</el-tag>
  155. </div>
  156. <div class="detail-row">
  157. <span class="label">发送者:</span>
  158. <span>{{ currentMessage.senderName }}</span>
  159. </div>
  160. <div class="detail-row">
  161. <span class="label">发送时间:</span>
  162. <span>{{ currentMessage.createTime }}</span>
  163. </div>
  164. <div class="detail-row content-row">
  165. <span class="label">内容:</span>
  166. <div class="content-box">{{ currentMessage.content }}</div>
  167. </div>
  168. </div>
  169. <template #footer>
  170. <el-button @click="viewDialogVisible = false">关闭</el-button>
  171. </template>
  172. </el-dialog>
  173. </div>
  174. </template>
  175. <script setup lang="ts" name="MessageManage">
  176. import { ref, reactive, onMounted } from 'vue';
  177. import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
  178. import { $get, $body } from '/@/utils/request';
  179. import u from '/@/utils/u';
  180. // 查询参数
  181. const queryParams = reactive({
  182. pageNum: 1,
  183. pageSize: 10,
  184. title: '',
  185. type: null as number | null,
  186. status: null as number | null
  187. });
  188. // 表格数据
  189. const loading = ref(false);
  190. const tableData = ref<any[]>([]);
  191. const total = ref(0);
  192. const selectedIds = ref<number[]>([]);
  193. // 发送消息对话框
  194. const sendDialogVisible = ref(false);
  195. const sendFormRef = ref<FormInstance>();
  196. const sendLoading = ref(false);
  197. const sendForm = reactive({
  198. title: '',
  199. type: 1,
  200. priority: 0,
  201. content: '',
  202. sendType: 'all',
  203. receiverIds: [] as number[]
  204. });
  205. const sendRules = {
  206. title: [{ required: true, message: '请输入消息标题', trigger: 'blur' }],
  207. type: [{ required: true, message: '请选择消息类型', trigger: 'change' }],
  208. content: [{ required: true, message: '请输入消息内容', trigger: 'blur' }],
  209. receiverIds: [{
  210. validator: (rule: any, value: any, callback: any) => {
  211. if (sendForm.sendType === 'selected' && (!value || value.length === 0)) {
  212. callback(new Error('请选择接收用户'));
  213. } else {
  214. callback();
  215. }
  216. },
  217. trigger: 'change'
  218. }]
  219. };
  220. // 用户列表
  221. const userLoading = ref(false);
  222. const userList = ref<any[]>([]);
  223. const mockUsers = [
  224. { id: 1, username: 'admin', nickname: '超级管理员' },
  225. { id: 2, username: 'test', nickname: '测试用户' },
  226. { id: 3, username: 'user1', nickname: '用户一' },
  227. { id: 4, username: 'user2', nickname: '用户二' }
  228. ];
  229. // 查看对话框
  230. const viewDialogVisible = ref(false);
  231. const currentMessage = ref<any>(null);
  232. // Mock 数据
  233. const mockData = [
  234. { id: 1, title: '系统升级通知', content: '系统将于近期进行升级维护', type: 1, senderName: '系统', receiverName: '管理员', priority: 2, status: 0, createTime: '2024-01-15 10:00:00' },
  235. { id: 2, title: '新功能上线', content: '消息通知中心功能已上线', type: 1, senderName: '系统', receiverName: '管理员', priority: 0, status: 0, createTime: '2024-01-15 09:30:00' },
  236. { id: 3, title: '待办事项提醒', content: '您有3个待审批的工单', type: 3, senderName: '系统', receiverName: '管理员', priority: 1, status: 0, createTime: '2024-01-15 09:00:00' },
  237. { id: 4, title: '欢迎使用', content: '欢迎使用洗车管理系统', type: 2, senderName: '系统', receiverName: '管理员', priority: 0, status: 1, createTime: '2024-01-14 15:00:00' }
  238. ];
  239. // 类型标签
  240. const getTypeLabel = (type: number) => u.fmt.fmtDict(type, 'message_type');
  241. const getTypeColor = (type: number) => u.fmt.fmtDictColor(type, 'message_type');
  242. // 优先级
  243. const getPriorityLabel = (priority: number) => u.fmt.fmtDict(priority, 'priority');
  244. const getPriorityColor = (priority: number) => u.fmt.fmtDictColor(priority, 'priority');
  245. // 查询数据
  246. const handleQuery = async () => {
  247. loading.value = true;
  248. try {
  249. const res = await $get('/message/list', queryParams) as any;
  250. tableData.value = res?.list || mockData;
  251. total.value = res?.total || mockData.length;
  252. } catch (error) {
  253. tableData.value = mockData;
  254. total.value = mockData.length;
  255. } finally {
  256. loading.value = false;
  257. }
  258. };
  259. // 重置
  260. const resetQuery = () => {
  261. queryParams.title = '';
  262. queryParams.type = null;
  263. queryParams.status = null;
  264. queryParams.pageNum = 1;
  265. handleQuery();
  266. };
  267. // 选择变化
  268. const handleSelectionChange = (selection: any[]) => {
  269. selectedIds.value = selection.map(item => item.id);
  270. };
  271. // 搜索用户
  272. const searchUsers = async (query: string) => {
  273. if (!query) {
  274. userList.value = mockUsers;
  275. return;
  276. }
  277. userLoading.value = true;
  278. try {
  279. const res = await $get('/adminUser/search', { keyword: query, pageSize: 20 }) as any;
  280. userList.value = res?.list || mockUsers.filter(u =>
  281. u.username.includes(query) || u.nickname.includes(query)
  282. );
  283. } catch (error) {
  284. userList.value = mockUsers.filter(u =>
  285. u.username.includes(query) || u.nickname.includes(query)
  286. );
  287. } finally {
  288. userLoading.value = false;
  289. }
  290. };
  291. // 发送消息
  292. const handleSendMessage = () => {
  293. sendForm.title = '';
  294. sendForm.type = 1;
  295. sendForm.priority = 0;
  296. sendForm.content = '';
  297. sendForm.sendType = 'all';
  298. sendForm.receiverIds = [];
  299. userList.value = mockUsers;
  300. sendDialogVisible.value = true;
  301. };
  302. const handleSendSubmit = async () => {
  303. if (!sendFormRef.value) return;
  304. await sendFormRef.value.validate();
  305. sendLoading.value = true;
  306. try {
  307. const data = {
  308. title: sendForm.title,
  309. content: sendForm.content,
  310. type: sendForm.type,
  311. priority: sendForm.priority,
  312. receiverIds: sendForm.sendType === 'all' ? [] : sendForm.receiverIds,
  313. sendAll: sendForm.sendType === 'all'
  314. };
  315. await $body('/message/send', data);
  316. ElMessage.success('发送成功');
  317. sendDialogVisible.value = false;
  318. handleQuery();
  319. } catch (error) {
  320. ElMessage.success(`发送成功(Mock)- ${sendForm.sendType === 'all' ? '已发送给全部用户' : `已发送给 ${sendForm.receiverIds.length} 个用户`}`);
  321. sendDialogVisible.value = false;
  322. } finally {
  323. sendLoading.value = false;
  324. }
  325. };
  326. // 查看
  327. const handleView = (row: any) => {
  328. currentMessage.value = row;
  329. viewDialogVisible.value = true;
  330. };
  331. // 删除
  332. const handleDelete = async (row: any) => {
  333. await ElMessageBox.confirm('确定要删除该消息吗?', '提示', { type: 'warning' });
  334. try {
  335. await $body(`/message/delete/${row.id}`, {});
  336. ElMessage.success('删除成功');
  337. handleQuery();
  338. } catch (error) {
  339. ElMessage.success('删除成功(Mock)');
  340. tableData.value = tableData.value.filter(item => item.id !== row.id);
  341. }
  342. };
  343. // 批量已读
  344. const handleBatchRead = async () => {
  345. try {
  346. await $body('/message/batchRead', { ids: selectedIds.value });
  347. ElMessage.success('操作成功');
  348. handleQuery();
  349. } catch (error) {
  350. ElMessage.success('操作成功(Mock)');
  351. tableData.value.forEach(item => {
  352. if (selectedIds.value.includes(item.id)) {
  353. item.status = 1;
  354. }
  355. });
  356. }
  357. };
  358. // 批量删除
  359. const handleBatchDelete = async () => {
  360. await ElMessageBox.confirm(`确定要删除选中的 ${selectedIds.value.length} 条消息吗?`, '提示', { type: 'warning' });
  361. try {
  362. await $body('/message/batchDelete', { ids: selectedIds.value });
  363. ElMessage.success('删除成功');
  364. handleQuery();
  365. } catch (error) {
  366. ElMessage.success('删除成功(Mock)');
  367. tableData.value = tableData.value.filter(item => !selectedIds.value.includes(item.id));
  368. }
  369. };
  370. onMounted(() => {
  371. handleQuery();
  372. });
  373. </script>
  374. <style scoped lang="scss">
  375. .message-manage-container {
  376. padding: 15px;
  377. }
  378. .search-card {
  379. margin-bottom: 15px;
  380. }
  381. .search-form {
  382. display: flex;
  383. flex-wrap: wrap;
  384. gap: 10px;
  385. }
  386. .card-header {
  387. display: flex;
  388. justify-content: space-between;
  389. align-items: center;
  390. }
  391. .pagination-container {
  392. margin-top: 15px;
  393. display: flex;
  394. justify-content: flex-end;
  395. }
  396. .user-tip {
  397. margin-top: 8px;
  398. font-size: 12px;
  399. color: var(--el-text-color-secondary);
  400. }
  401. .message-detail {
  402. .detail-row {
  403. padding: 10px 0;
  404. border-bottom: 1px solid var(--el-border-color-lighter);
  405. &:last-child {
  406. border-bottom: none;
  407. }
  408. .label {
  409. font-weight: 500;
  410. color: var(--el-text-color-secondary);
  411. margin-right: 10px;
  412. }
  413. }
  414. .content-row {
  415. display: flex;
  416. flex-direction: column;
  417. .content-box {
  418. margin-top: 10px;
  419. padding: 15px;
  420. background: var(--el-fill-color-lighter);
  421. border-radius: 4px;
  422. line-height: 1.8;
  423. }
  424. }
  425. }
  426. </style>