Commit 6a1f8b67b7a3c542ad79db1a9814ffab52631184

Authored by shaofan
1 parent a8d4dc32

新增消息中心

src/api/index.ts
@@ -230,3 +230,11 @@ export const locationApi = { @@ -230,3 +230,11 @@ export const locationApi = {
230 nearby: (cityId: number, lng: string, lat: string) => 230 nearby: (cityId: number, lng: string, lat: string) =>
231 request.get('/api/rider/location/nearby', { params: { cityId, lng, lat } }), 231 request.get('/api/rider/location/nearby', { params: { cityId, lng, lat } }),
232 } 232 }
  233 +
  234 +// 消息管理
  235 +export const messageApi = {
  236 + list: (params: any) => request.get('/api/admin/message/list', { params }),
  237 + send: (data: any) => request.post('/api/admin/message/send', data),
  238 + broadcast: (data: any) => request.post('/api/admin/message/broadcast', data),
  239 + templates: () => request.get('/api/admin/message/templates'),
  240 +}
src/router/index.ts
@@ -153,6 +153,12 @@ const router = createRouter({ @@ -153,6 +153,12 @@ const router = createRouter({
153 component: () => import('@/views/admin/AdminUserList.vue'), 153 component: () => import('@/views/admin/AdminUserList.vue'),
154 meta: { title: '平台账号' }, 154 meta: { title: '平台账号' },
155 }, 155 },
  156 + {
  157 + path: 'message',
  158 + name: 'Message',
  159 + component: () => import('@/views/message/MessageList.vue'),
  160 + meta: { title: '消息管理' },
  161 + },
156 ], 162 ],
157 }, 163 },
158 { path: '/:pathMatch(.*)*', redirect: '/' }, 164 { path: '/:pathMatch(.*)*', redirect: '/' },
src/views/message/MessageList.vue 0 → 100644
  1 +<template>
  2 + <div>
  3 + <a-card title="消息管理" :bordered="false">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-input
  7 + v-model:value="searchForm.riderId"
  8 + placeholder="骑手ID"
  9 + style="width: 150px"
  10 + allow-clear
  11 + />
  12 + <a-select
  13 + v-model:value="searchForm.type"
  14 + placeholder="消息类型"
  15 + style="width: 120px"
  16 + allow-clear
  17 + >
  18 + <a-select-option :value="1">订单消息</a-select-option>
  19 + <a-select-option :value="2">系统通知</a-select-option>
  20 + </a-select>
  21 + <a-button type="primary" @click="loadList">查询</a-button>
  22 + </div>
  23 + <div class="list-toolbar-right">
  24 + <a-button type="primary" @click="openSend">发送消息</a-button>
  25 + <a-button @click="openBroadcast">群发消息</a-button>
  26 + </div>
  27 + </div>
  28 +
  29 + <a-table
  30 + :dataSource="list"
  31 + :columns="columns"
  32 + :loading="loading"
  33 + :pagination="pagination"
  34 + @change="handleTableChange"
  35 + rowKey="id"
  36 + >
  37 + <template #bodyCell="{ column, record }">
  38 + <template v-if="column.key === 'type'">
  39 + <a-tag :color="record.type === 1 ? 'blue' : 'orange'">
  40 + {{ record.type === 1 ? '订单' : '系统' }}
  41 + </a-tag>
  42 + </template>
  43 + <template v-if="column.key === 'isRead'">
  44 + <a-tag :color="record.isRead === 1 ? 'green' : 'default'">
  45 + {{ record.isRead === 1 ? '已读' : '未读' }}
  46 + </a-tag>
  47 + </template>
  48 + <template v-if="column.key === 'createTime'">
  49 + {{ formatTime(record.createTime) }}
  50 + </template>
  51 + </template>
  52 + </a-table>
  53 + </a-card>
  54 +
  55 + <!-- 发送消息模态框 -->
  56 + <a-modal
  57 + v-model:open="sendVisible"
  58 + title="发送消息"
  59 + @ok="handleSend"
  60 + :confirmLoading="sending"
  61 + >
  62 + <a-form :model="sendForm" layout="vertical">
  63 + <a-form-item label="骑手ID" required>
  64 + <a-input-number v-model:value="sendForm.riderId" style="width: 100%" placeholder="请输入骑手ID" />
  65 + </a-form-item>
  66 + <a-form-item label="消息类型" required>
  67 + <a-radio-group v-model:value="sendForm.type">
  68 + <a-radio :value="1">订单消息</a-radio>
  69 + <a-radio :value="2">系统通知</a-radio>
  70 + </a-radio-group>
  71 + </a-form-item>
  72 + <a-form-item label="标题" required>
  73 + <a-input v-model:value="sendForm.title" placeholder="请输入消息标题" />
  74 + </a-form-item>
  75 + <a-form-item label="内容" required>
  76 + <a-textarea v-model:value="sendForm.content" :rows="4" placeholder="请输入消息内容" />
  77 + </a-form-item>
  78 + </a-form>
  79 + </a-modal>
  80 +
  81 + <!-- 群发消息模态框 -->
  82 + <a-modal
  83 + v-model:open="broadcastVisible"
  84 + title="群发消息"
  85 + @ok="handleBroadcast"
  86 + :confirmLoading="broadcasting"
  87 + >
  88 + <a-alert
  89 + message="将发送给当前城市所有正常状态的骑手"
  90 + type="warning"
  91 + show-icon
  92 + style="margin-bottom: 16px"
  93 + />
  94 + <a-form :model="broadcastForm" layout="vertical">
  95 + <a-form-item label="消息类型" required>
  96 + <a-radio-group v-model:value="broadcastForm.type">
  97 + <a-radio :value="1">订单消息</a-radio>
  98 + <a-radio :value="2">系统通知</a-radio>
  99 + </a-radio-group>
  100 + </a-form-item>
  101 + <a-form-item label="标题" required>
  102 + <a-input v-model:value="broadcastForm.title" placeholder="请输入消息标题" />
  103 + </a-form-item>
  104 + <a-form-item label="内容" required>
  105 + <a-textarea v-model:value="broadcastForm.content" :rows="4" placeholder="请输入消息内容" />
  106 + </a-form-item>
  107 + </a-form>
  108 + </a-modal>
  109 + </div>
  110 +</template>
  111 +
  112 +<script setup lang="ts">
  113 +import { onMounted, reactive, ref, computed } from 'vue'
  114 +import { message } from 'ant-design-vue'
  115 +import { messageApi } from '@/api'
  116 +import type { TablePaginationConfig } from 'ant-design-vue'
  117 +
  118 +const loading = ref(false)
  119 +const sending = ref(false)
  120 +const broadcasting = ref(false)
  121 +const list = ref<any[]>([])
  122 +const sendVisible = ref(false)
  123 +const broadcastVisible = ref(false)
  124 +
  125 +const searchForm = reactive({
  126 + riderId: undefined as number | undefined,
  127 + type: undefined as number | undefined,
  128 +})
  129 +
  130 +const sendForm = reactive({
  131 + riderId: undefined as number | undefined,
  132 + type: 2,
  133 + title: '',
  134 + content: '',
  135 +})
  136 +
  137 +const broadcastForm = reactive({
  138 + type: 2,
  139 + title: '',
  140 + content: '',
  141 +})
  142 +
  143 +const currentPage = ref(1)
  144 +const pageSize = ref(20)
  145 +const total = ref(0)
  146 +
  147 +const pagination = computed<TablePaginationConfig>(() => ({
  148 + current: currentPage.value,
  149 + pageSize: pageSize.value,
  150 + total: total.value,
  151 + showSizeChanger: false,
  152 + showTotal: (count) => `共 ${count} 条`,
  153 +}))
  154 +
  155 +const columns = [
  156 + { title: 'ID', dataIndex: 'id', width: 80 },
  157 + { title: '骑手ID', dataIndex: 'riderId', width: 100 },
  158 + { title: '类型', key: 'type', width: 100 },
  159 + { title: '标题', dataIndex: 'title', width: 150 },
  160 + { title: '内容', dataIndex: 'content', ellipsis: true },
  161 + { title: '状态', key: 'isRead', width: 80 },
  162 + { title: '创建时间', key: 'createTime', width: 180 },
  163 +]
  164 +
  165 +async function loadList() {
  166 + loading.value = true
  167 + try {
  168 + const res: any = await messageApi.list({
  169 + riderId: searchForm.riderId,
  170 + type: searchForm.type,
  171 + page: currentPage.value,
  172 + })
  173 + const data = res.data || {}
  174 + list.value = Array.isArray(data.list) ? data.list : []
  175 + total.value = Number(data.total || 0)
  176 + currentPage.value = Number(data.page || currentPage.value)
  177 + pageSize.value = Number(data.pageSize || 20)
  178 + } finally {
  179 + loading.value = false
  180 + }
  181 +}
  182 +
  183 +function handleTableChange(pag: TablePaginationConfig) {
  184 + const nextPage = pag.current ?? 1
  185 + if (nextPage === currentPage.value) return
  186 + currentPage.value = nextPage
  187 + loadList()
  188 +}
  189 +
  190 +function openSend() {
  191 + Object.assign(sendForm, { riderId: undefined, type: 2, title: '', content: '' })
  192 + sendVisible.value = true
  193 +}
  194 +
  195 +function openBroadcast() {
  196 + Object.assign(broadcastForm, { type: 2, title: '', content: '' })
  197 + broadcastVisible.value = true
  198 +}
  199 +
  200 +async function handleSend() {
  201 + if (!sendForm.riderId || !sendForm.title || !sendForm.content) {
  202 + message.error('请填写完整信息')
  203 + return
  204 + }
  205 + sending.value = true
  206 + try {
  207 + await messageApi.send(sendForm)
  208 + message.success('发送成功')
  209 + sendVisible.value = false
  210 + loadList()
  211 + } finally {
  212 + sending.value = false
  213 + }
  214 +}
  215 +
  216 +async function handleBroadcast() {
  217 + if (!broadcastForm.title || !broadcastForm.content) {
  218 + message.error('请填写完整信息')
  219 + return
  220 + }
  221 + broadcasting.value = true
  222 + try {
  223 + await messageApi.broadcast(broadcastForm)
  224 + message.success('群发成功')
  225 + broadcastVisible.value = false
  226 + loadList()
  227 + } finally {
  228 + broadcasting.value = false
  229 + }
  230 +}
  231 +
  232 +function formatTime(timestamp: number) {
  233 + if (!timestamp) return '-'
  234 + const date = new Date(timestamp)
  235 + return date.toLocaleString('zh-CN', {
  236 + year: 'numeric',
  237 + month: '2-digit',
  238 + day: '2-digit',
  239 + hour: '2-digit',
  240 + minute: '2-digit',
  241 + second: '2-digit',
  242 + })
  243 +}
  244 +
  245 +onMounted(loadList)
  246 +</script>
  247 +
  248 +<style scoped>
  249 +.list-toolbar {
  250 + display: flex;
  251 + justify-content: space-between;
  252 + margin-bottom: 16px;
  253 +}
  254 +
  255 +.list-toolbar-left {
  256 + display: flex;
  257 + gap: 8px;
  258 +}
  259 +
  260 +.list-toolbar-right {
  261 + display: flex;
  262 + gap: 8px;
  263 +}
  264 +</style>