MessageList.vue 9.73 KB
<template>
  <div>
    <a-card title="消息管理" :bordered="false">
      <div class="list-toolbar">
        <div class="list-toolbar-left">
          <a-select
            v-if="isAdmin"
            v-model:value="selectedCityId"
            placeholder="请选择租户"
            style="width: 180px"
            @change="handleCityChange"
          >
            <a-select-option v-for="item in cityList" :key="item.id" :value="item.id">
              {{ item.name }}
            </a-select-option>
          </a-select>
          <div v-else class="managed-city-pill">当前租户:{{ currentCityName }}</div>
          <a-input
            v-model:value="searchForm.riderId"
            placeholder="骑手ID"
            style="width: 150px"
            allow-clear
          />
          <a-select
            v-model:value="searchForm.type"
            placeholder="消息类型"
            style="width: 120px"
            allow-clear
          >
            <a-select-option :value="1">订单消息</a-select-option>
            <a-select-option :value="2">系统通知</a-select-option>
          </a-select>
          <a-button type="primary" :disabled="!canQuery" @click="handleSearch">查询</a-button>
        </div>
        <div class="list-toolbar-right">
          <a-button type="primary" :disabled="!canQuery" @click="openSend">发送消息</a-button>
          <a-button :disabled="!canQuery" @click="openBroadcast">群发消息</a-button>
        </div>
      </div>

      <a-alert
        v-if="isAdmin"
        type="info"
        show-icon
        message="平台管理员需要先选择租户,消息列表、单发和群发都会限定在所选租户内。"
        style="margin-bottom: 16px"
      />

      <a-table
        :dataSource="list"
        :columns="columns"
        :loading="loading"
        :pagination="pagination"
        @change="handleTableChange"
        rowKey="id"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'type'">
            <a-tag :color="record.type === 1 ? 'blue' : 'orange'">
              {{ record.type === 1 ? '订单' : '系统' }}
            </a-tag>
          </template>
          <template v-if="column.key === 'isRead'">
            <a-tag :color="record.isRead === 1 ? 'green' : 'default'">
              {{ record.isRead === 1 ? '已读' : '未读' }}
            </a-tag>
          </template>
          <template v-if="column.key === 'createTime'">
            {{ formatTime(record.createTime) }}
          </template>
        </template>
      </a-table>
    </a-card>

    <!-- 发送消息模态框 -->
    <a-modal
      v-model:open="sendVisible"
      title="发送消息"
      @ok="handleSend"
      :confirmLoading="sending"
    >
      <a-alert
        :message="`将发送给当前租户(${currentCityName})下的指定骑手`"
        type="info"
        show-icon
        style="margin-bottom: 16px"
      />
      <a-form :model="sendForm" layout="vertical">
        <a-form-item label="骑手ID" required>
          <a-input-number v-model:value="sendForm.riderId" style="width: 100%" placeholder="请输入骑手ID" />
        </a-form-item>
        <a-form-item label="消息类型" required>
          <a-radio-group v-model:value="sendForm.type">
            <a-radio :value="1">订单消息</a-radio>
            <a-radio :value="2">系统通知</a-radio>
          </a-radio-group>
        </a-form-item>
        <a-form-item label="标题" required>
          <a-input v-model:value="sendForm.title" placeholder="请输入消息标题" />
        </a-form-item>
        <a-form-item label="内容" required>
          <a-textarea v-model:value="sendForm.content" :rows="4" placeholder="请输入消息内容" />
        </a-form-item>
      </a-form>
    </a-modal>

    <!-- 群发消息模态框 -->
    <a-modal
      v-model:open="broadcastVisible"
      title="群发消息"
      @ok="handleBroadcast"
      :confirmLoading="broadcasting"
    >
      <a-alert
        :message="`将发送给当前租户(${currentCityName})所有正常状态的骑手`"
        type="warning"
        show-icon
        style="margin-bottom: 16px"
      />
      <a-form :model="broadcastForm" layout="vertical">
        <a-form-item label="消息类型" required>
          <a-radio-group v-model:value="broadcastForm.type">
            <a-radio :value="1">订单消息</a-radio>
            <a-radio :value="2">系统通知</a-radio>
          </a-radio-group>
        </a-form-item>
        <a-form-item label="标题" required>
          <a-input v-model:value="broadcastForm.title" placeholder="请输入消息标题" />
        </a-form-item>
        <a-form-item label="内容" required>
          <a-textarea v-model:value="broadcastForm.content" :rows="4" placeholder="请输入消息内容" />
        </a-form-item>
      </a-form>
    </a-modal>
  </div>
</template>

<script setup lang="ts">
import { onMounted, reactive, ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import { messageApi } from '@/api'
import { useRoleCityList } from '@/composables/useRoleCityList'
import type { TablePaginationConfig } from 'ant-design-vue'

const { isAdmin, managedCityId, managedCityName, cityList, loadCities, getCityName } = useRoleCityList()

const loading = ref(false)
const sending = ref(false)
const broadcasting = ref(false)
const list = ref<any[]>([])
const sendVisible = ref(false)
const broadcastVisible = ref(false)
const selectedCityId = ref<number | undefined>()

const searchForm = reactive({
  riderId: undefined as number | undefined,
  type: undefined as number | undefined,
})

const sendForm = reactive({
  riderId: undefined as number | undefined,
  type: 2,
  title: '',
  content: '',
})

const broadcastForm = reactive({
  type: 2,
  title: '',
  content: '',
})

const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const canQuery = computed(() => !!selectedCityId.value)
const currentCityName = computed(() => selectedCityId.value ? getCityName(selectedCityId.value) : managedCityName.value || '-')

const pagination = computed<TablePaginationConfig>(() => ({
  current: currentPage.value,
  pageSize: pageSize.value,
  total: total.value,
  showSizeChanger: false,
  showTotal: (count) => `共 ${count} 条`,
}))

const columns = [
  { title: 'ID', dataIndex: 'id', width: 80 },
  { title: '骑手ID', dataIndex: 'riderId', width: 100 },
  { title: '类型', key: 'type', width: 100 },
  { title: '标题', dataIndex: 'title', width: 150 },
  { title: '内容', dataIndex: 'content', ellipsis: true },
  { title: '状态', key: 'isRead', width: 80 },
  { title: '创建时间', key: 'createTime', width: 180 },
]

async function loadList() {
  if (!selectedCityId.value) {
    list.value = []
    total.value = 0
    return
  }
  loading.value = true
  try {
    const res: any = await messageApi.list({
      cityId: selectedCityId.value,
      riderId: searchForm.riderId,
      type: searchForm.type,
      page: currentPage.value,
    })
    const data = res.data || {}
    list.value = Array.isArray(data.list) ? data.list : []
    total.value = Number(data.total || 0)
    currentPage.value = Number(data.page || currentPage.value)
    pageSize.value = Number(data.pageSize || 20)
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  currentPage.value = 1
  loadList()
}

function handleCityChange() {
  currentPage.value = 1
  loadList()
}

function handleTableChange(pag: TablePaginationConfig) {
  const nextPage = pag.current ?? 1
  if (nextPage === currentPage.value) return
  currentPage.value = nextPage
  loadList()
}

function ensureCitySelected() {
  if (!selectedCityId.value) {
    message.error('请先选择租户')
    return false
  }
  return true
}

function openSend() {
  if (!ensureCitySelected()) return
  Object.assign(sendForm, { riderId: undefined, type: 2, title: '', content: '' })
  sendVisible.value = true
}

function openBroadcast() {
  if (!ensureCitySelected()) return
  Object.assign(broadcastForm, { type: 2, title: '', content: '' })
  broadcastVisible.value = true
}

async function handleSend() {
  if (!ensureCitySelected()) return
  if (!sendForm.riderId || !sendForm.title || !sendForm.content) {
    message.error('请填写完整信息')
    return
  }
  sending.value = true
  try {
    await messageApi.send({ ...sendForm, cityId: selectedCityId.value })
    message.success('发送成功')
    sendVisible.value = false
    loadList()
  } finally {
    sending.value = false
  }
}

async function handleBroadcast() {
  if (!ensureCitySelected()) return
  if (!broadcastForm.title || !broadcastForm.content) {
    message.error('请填写完整信息')
    return
  }
  broadcasting.value = true
  try {
    await messageApi.broadcast({ ...broadcastForm, cityId: selectedCityId.value })
    message.success('群发成功')
    broadcastVisible.value = false
    loadList()
  } finally {
    broadcasting.value = false
  }
}

function formatTime(timestamp: number) {
  if (!timestamp) return '-'
  const date = new Date(timestamp)
  return date.toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
  })
}

onMounted(async () => {
  await loadCities()
  if (!isAdmin.value) {
    selectedCityId.value = managedCityId.value
    await loadList()
  }
})
</script>

<style scoped>
.list-toolbar {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
}

.list-toolbar-left {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.list-toolbar-right {
  display: flex;
  gap: 8px;
}

.managed-city-pill {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel-strong);
  color: var(--text-main);
  font-size: 13px;
}
</style>