index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <script setup lang="ts">
  2. import { reactive, onMounted, ref, nextTick } from "vue";
  3. import { getMessageList, sendMessage, deleteMessage, batchReadMessage, batchDeleteMessage, searchAdminUser } from "@/api/message";
  4. import { useRenderIcon } from "@/components/ReIcon/src/hooks";
  5. import { ElMessage, ElMessageBox } from "element-plus";
  6. import { getDictOptions, formatDict, getDictColor } from "@/utils/dict";
  7. defineOptions({ name: "AdminMessage" });
  8. const queryRef = ref();
  9. const state = reactive({
  10. formQuery: {
  11. title: "",
  12. type: "" as number | string,
  13. status: "" as number | string
  14. },
  15. pageQuery: {
  16. pageNum: 1,
  17. pageSize: 10,
  18. total: 0
  19. },
  20. tableData: {
  21. height: 500,
  22. data: [] as Array<any>,
  23. loading: false
  24. },
  25. selectedIds: [] as number[]
  26. });
  27. // 发送消息弹窗
  28. const sendVisible = ref(false);
  29. const sendLoading = ref(false);
  30. const sendForm = reactive({
  31. title: "",
  32. type: 1,
  33. priority: 0,
  34. sendType: "all" as "all" | "selected",
  35. receiverIds: [] as number[],
  36. content: ""
  37. });
  38. const userOptions = ref<any[]>([]);
  39. // 详情弹窗
  40. const detailVisible = ref(false);
  41. const currentMsg = ref<any>(null);
  42. const typeOptions = () => getDictOptions("message_type");
  43. const priorityOptions = () => getDictOptions("priority");
  44. onMounted(() => {
  45. loadData();
  46. nextTick(() => {
  47. const bodyHeight = document.body.clientHeight;
  48. const queryHeight = queryRef.value?.$el?.clientHeight || 0;
  49. state.tableData.height = bodyHeight - queryHeight - 280;
  50. });
  51. });
  52. const loadData = (refresh: boolean = false) => {
  53. if (refresh) {
  54. state.pageQuery.pageNum = 1;
  55. }
  56. state.tableData.loading = true;
  57. getMessageList({ ...state.formQuery, ...state.pageQuery })
  58. .then((res: any) => {
  59. const { list, total } = res || {};
  60. state.tableData.data = list || [];
  61. state.pageQuery.total = total || 0;
  62. })
  63. .catch(() => {
  64. state.tableData.data = [];
  65. })
  66. .finally(() => {
  67. state.tableData.loading = false;
  68. });
  69. };
  70. const handleSizeChange = (size: number) => {
  71. state.pageQuery.pageSize = size;
  72. loadData(true);
  73. };
  74. const handleCurrentChange = (page: number) => {
  75. state.pageQuery.pageNum = page;
  76. loadData();
  77. };
  78. const handleSearch = () => {
  79. loadData(true);
  80. };
  81. const handleReset = () => {
  82. state.formQuery = {
  83. title: "",
  84. type: "",
  85. status: ""
  86. };
  87. loadData(true);
  88. };
  89. const handleSelectionChange = (rows: any[]) => {
  90. state.selectedIds = rows.map(k => k.id);
  91. };
  92. const handleView = (row: any) => {
  93. currentMsg.value = row;
  94. detailVisible.value = true;
  95. };
  96. const handleDelete = (row: any) => {
  97. ElMessageBox.confirm("确定要删除此消息吗?", "提示", {
  98. confirmButtonText: "确定",
  99. cancelButtonText: "取消",
  100. type: "warning"
  101. }).then(() => {
  102. deleteMessage(row.id).then(() => {
  103. ElMessage.success("删除成功");
  104. loadData(true);
  105. });
  106. }).catch(() => {});
  107. };
  108. const handleBatchRead = () => {
  109. if (state.selectedIds.length === 0) {
  110. ElMessage.warning("请选择消息");
  111. return;
  112. }
  113. batchReadMessage({ ids: state.selectedIds }).then(() => {
  114. ElMessage.success("已标记为已读");
  115. loadData(true);
  116. });
  117. };
  118. const handleBatchDelete = () => {
  119. if (state.selectedIds.length === 0) {
  120. ElMessage.warning("请选择消息");
  121. return;
  122. }
  123. ElMessageBox.confirm("确定要批量删除选中的消息吗?", "提示", {
  124. confirmButtonText: "确定",
  125. cancelButtonText: "取消",
  126. type: "warning"
  127. }).then(() => {
  128. batchDeleteMessage({ ids: state.selectedIds }).then(() => {
  129. ElMessage.success("删除成功");
  130. loadData(true);
  131. });
  132. }).catch(() => {});
  133. };
  134. // 发送消息
  135. const handleOpenSend = () => {
  136. Object.assign(sendForm, {
  137. title: "",
  138. type: 1,
  139. priority: 0,
  140. sendType: "all",
  141. receiverIds: [],
  142. content: ""
  143. });
  144. userOptions.value = [];
  145. sendVisible.value = true;
  146. };
  147. const handleSendTypeChange = (val: string) => {
  148. if (val !== "selected") {
  149. sendForm.receiverIds = [];
  150. }
  151. };
  152. const searchUsers = (query: string) => {
  153. if (!query) {
  154. userOptions.value = [];
  155. return;
  156. }
  157. searchAdminUser({ keyword: query, pageSize: 20 }).then((res: any) => {
  158. userOptions.value = (res || []).map((k: any) => ({
  159. label: `${k.nickname || k.username} (${k.mobilePhone || ""})`,
  160. value: k.id || k.userId
  161. }));
  162. });
  163. };
  164. const handleSendSubmit = async () => {
  165. if (!sendForm.title || !sendForm.content) {
  166. ElMessage.warning("请填写标题和内容");
  167. return;
  168. }
  169. sendLoading.value = true;
  170. try {
  171. await sendMessage({
  172. title: sendForm.title,
  173. content: sendForm.content,
  174. type: sendForm.type,
  175. priority: sendForm.priority,
  176. sendAll: sendForm.sendType === "all",
  177. receiverIds: sendForm.sendType === "selected" ? sendForm.receiverIds : []
  178. });
  179. ElMessage.success("发送成功");
  180. sendVisible.value = false;
  181. loadData(true);
  182. } catch (e) {
  183. ElMessage.error("发送失败");
  184. } finally {
  185. sendLoading.value = false;
  186. }
  187. };
  188. </script>
  189. <template>
  190. <div class="page-container">
  191. <el-card shadow="hover">
  192. <!-- 搜索栏 -->
  193. <el-form ref="queryRef" :model="state.formQuery" inline class="search-form">
  194. <el-form-item label="标题">
  195. <el-input
  196. v-model="state.formQuery.title"
  197. placeholder="请输入标题"
  198. clearable
  199. style="width: 180px"
  200. @keyup.enter="handleSearch"
  201. />
  202. </el-form-item>
  203. <el-form-item label="类型">
  204. <el-select v-model="state.formQuery.type" placeholder="请选择" clearable style="width: 140px">
  205. <el-option v-for="opt in typeOptions()" :key="opt.value" :label="opt.label" :value="opt.value" />
  206. </el-select>
  207. </el-form-item>
  208. <el-form-item label="状态">
  209. <el-select v-model="state.formQuery.status" placeholder="请选择" clearable style="width: 120px">
  210. <el-option v-for="opt in getDictOptions('message_status')" :key="opt.value" :label="opt.label" :value="opt.value" />
  211. </el-select>
  212. </el-form-item>
  213. <el-form-item>
  214. <el-button type="primary" :icon="useRenderIcon('ri/search-line')" @click="handleSearch">查询</el-button>
  215. <el-button :icon="useRenderIcon('ri/refresh-line')" @click="handleReset">重置</el-button>
  216. </el-form-item>
  217. </el-form>
  218. <!-- 操作栏 -->
  219. <div class="toolbar">
  220. <el-button type="primary" :icon="useRenderIcon('ri/add-line')" @click="handleOpenSend">发送消息</el-button>
  221. <el-button :icon="useRenderIcon('ri/mail-check-line')" :disabled="state.selectedIds.length === 0" @click="handleBatchRead">批量已读</el-button>
  222. <el-button type="danger" :icon="useRenderIcon('ri/delete-bin-line')" :disabled="state.selectedIds.length === 0" @click="handleBatchDelete">批量删除</el-button>
  223. </div>
  224. <!-- 表格 -->
  225. <el-table
  226. v-loading="state.tableData.loading"
  227. :data="state.tableData.data"
  228. :height="state.tableData.height"
  229. border
  230. stripe
  231. @selection-change="handleSelectionChange"
  232. >
  233. <template #empty>
  234. <el-empty description="暂无数据" />
  235. </template>
  236. <el-table-column type="selection" width="50" />
  237. <el-table-column label="ID" prop="id" width="80" />
  238. <el-table-column label="标题" prop="title" min-width="200" show-overflow-tooltip />
  239. <el-table-column label="类型" prop="type" width="120">
  240. <template #default="{ row }">
  241. <el-tag :type="getDictColor('message_type', row.type)" size="small">{{ formatDict('message_type', row.type) }}</el-tag>
  242. </template>
  243. </el-table-column>
  244. <el-table-column label="发送者" prop="senderName" width="100" />
  245. <el-table-column label="接收者" prop="receiverName" width="100" />
  246. <el-table-column label="优先级" prop="priority" width="100">
  247. <template #default="{ row }">
  248. <el-tag :type="getDictColor('priority', row.priority)" size="small">{{ formatDict('priority', row.priority) }}</el-tag>
  249. </template>
  250. </el-table-column>
  251. <el-table-column label="状态" prop="status" width="80">
  252. <template #default="{ row }">
  253. <el-tag :type="getDictColor('message_status', row.status)" size="small">{{ formatDict('message_status', row.status) }}</el-tag>
  254. </template>
  255. </el-table-column>
  256. <el-table-column label="发送时间" prop="createTime" width="170" />
  257. <el-table-column label="操作" width="120" fixed="right">
  258. <template #default="{ row }">
  259. <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
  260. <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
  261. </template>
  262. </el-table-column>
  263. </el-table>
  264. <div class="pagination-container">
  265. <el-pagination
  266. v-model:current-page="state.pageQuery.pageNum"
  267. v-model:page-size="state.pageQuery.pageSize"
  268. :total="state.pageQuery.total"
  269. :page-sizes="[10, 20, 50, 100]"
  270. layout="total, sizes, prev, pager, next, jumper"
  271. @size-change="handleSizeChange"
  272. @current-change="handleCurrentChange"
  273. />
  274. </div>
  275. </el-card>
  276. <!-- 发送消息弹窗 -->
  277. <el-dialog v-model="sendVisible" title="发送消息" width="650px" destroy-on-close>
  278. <el-form :model="sendForm" label-width="100px">
  279. <el-form-item label="标题" required>
  280. <el-input v-model="sendForm.title" placeholder="请输入消息标题" />
  281. </el-form-item>
  282. <el-form-item label="类型">
  283. <el-select v-model="sendForm.type" style="width: 100%">
  284. <el-option v-for="opt in typeOptions()" :key="opt.value" :label="opt.label" :value="opt.value" />
  285. </el-select>
  286. </el-form-item>
  287. <el-form-item label="优先级">
  288. <el-select v-model="sendForm.priority" style="width: 100%">
  289. <el-option v-for="opt in priorityOptions()" :key="opt.value" :label="opt.label" :value="opt.value" />
  290. </el-select>
  291. </el-form-item>
  292. <el-form-item label="发送方式">
  293. <el-radio-group v-model="sendForm.sendType" @change="handleSendTypeChange">
  294. <el-radio value="all">全部用户</el-radio>
  295. <el-radio value="selected">指定用户</el-radio>
  296. </el-radio-group>
  297. </el-form-item>
  298. <el-form-item label="接收者" v-if="sendForm.sendType === 'selected'">
  299. <el-select
  300. v-model="sendForm.receiverIds"
  301. multiple
  302. filterable
  303. remote
  304. reserve-keyword
  305. placeholder="搜索并选择用户"
  306. :remote-method="searchUsers"
  307. :loading="false"
  308. style="width: 100%"
  309. >
  310. <el-option v-for="user in userOptions" :key="user.value" :label="user.label" :value="user.value" />
  311. </el-select>
  312. </el-form-item>
  313. <el-form-item label="内容" required>
  314. <el-input v-model="sendForm.content" type="textarea" :rows="5" placeholder="请输入消息内容" />
  315. </el-form-item>
  316. </el-form>
  317. <template #footer>
  318. <el-button @click="sendVisible = false">取消</el-button>
  319. <el-button :loading="sendLoading" type="primary" @click="handleSendSubmit">发送</el-button>
  320. </template>
  321. </el-dialog>
  322. <!-- 消息详情弹窗 -->
  323. <el-dialog v-model="detailVisible" title="消息详情" width="600px" destroy-on-close>
  324. <template v-if="currentMsg">
  325. <el-descriptions :column="2" border>
  326. <el-descriptions-item label="标题">{{ currentMsg.title }}</el-descriptions-item>
  327. <el-descriptions-item label="类型">
  328. <el-tag :type="getDictColor('message_type', currentMsg.type)" size="small">{{ formatDict('message_type', currentMsg.type) }}</el-tag>
  329. </el-descriptions-item>
  330. <el-descriptions-item label="发送者">{{ currentMsg.senderName }}</el-descriptions-item>
  331. <el-descriptions-item label="发送时间">{{ currentMsg.createTime }}</el-descriptions-item>
  332. </el-descriptions>
  333. <div class="detail-section">
  334. <div class="section-title">消息内容</div>
  335. <div class="content-box">{{ currentMsg.content }}</div>
  336. </div>
  337. </template>
  338. <template #footer>
  339. <el-button @click="detailVisible = false">关闭</el-button>
  340. </template>
  341. </el-dialog>
  342. </div>
  343. </template>
  344. <style scoped lang="scss">
  345. .page-container {
  346. padding: 15px;
  347. }
  348. .search-form {
  349. margin-bottom: 15px;
  350. }
  351. .toolbar {
  352. margin-bottom: 15px;
  353. display: flex;
  354. gap: 10px;
  355. }
  356. .pagination-container {
  357. display: flex;
  358. justify-content: flex-end;
  359. margin-top: 15px;
  360. }
  361. .detail-section {
  362. margin-top: 15px;
  363. .section-title {
  364. font-weight: 500;
  365. margin-bottom: 10px;
  366. color: var(--el-text-color-primary);
  367. }
  368. .content-box {
  369. padding: 12px;
  370. background: var(--el-fill-color-lighter);
  371. border-radius: 4px;
  372. white-space: pre-wrap;
  373. line-height: 1.6;
  374. }
  375. }
  376. </style>