任务执行1-19
This commit is contained in:
@@ -8,6 +8,7 @@ import Chart from './chart/index.vue'
|
||||
import SearchForm from './search-form/index.vue'
|
||||
import DataTable from './data-table/index.vue'
|
||||
import SearchTable from './search-table/index.vue'
|
||||
import PageState from './page-state/index.vue'
|
||||
|
||||
// Manually introduce ECharts modules to reduce packing size
|
||||
|
||||
@@ -31,5 +32,6 @@ export default {
|
||||
Vue.component('SearchForm', SearchForm)
|
||||
Vue.component('DataTable', DataTable)
|
||||
Vue.component('SearchTable', SearchTable)
|
||||
Vue.component('PageState', PageState)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
<a-result v-if="!renderList.length" status="404">
|
||||
<template #subtitle>{{ $t('messageBox.noContent') }}</template>
|
||||
</a-result>
|
||||
<List :render-list="renderList" :unread-count="unreadCount" @item-click="handleItemClick" />
|
||||
<List
|
||||
:render-list="renderList"
|
||||
:unread-count="unreadCount"
|
||||
@item-click="handleItemClick"
|
||||
@resend="handleResend"
|
||||
@view-history="openHistory"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-button type="text" @click="emptyList">
|
||||
@@ -16,12 +22,33 @@
|
||||
</a-button>
|
||||
</template>
|
||||
</a-tabs>
|
||||
<a-drawer v-model:visible="historyVisible" width="760px" title="通知投递历史" :footer="false" unmount-on-close @open="fetchHistory">
|
||||
<a-table :columns="historyColumns" :data="historyList" :loading="historyLoading" :pagination="historyPagination" row-key="id" @page-change="onHistoryPageChange">
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getDeliveryColor(record.status)">
|
||||
{{ getDeliveryLabel(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<a-button v-if="canResendDelivery(record.status)" size="mini" type="text" @click="resendDelivery(record.id)">补发</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-drawer>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MessageListType, MessageRecord, queryMessageList, setMessageStatus } from '@/api/message'
|
||||
import {
|
||||
MessageListType,
|
||||
MessageRecord,
|
||||
NotificationDeliveryRecord,
|
||||
queryMessageList,
|
||||
queryNotificationDeliveryHistory,
|
||||
resendNotificationDelivery,
|
||||
setMessageStatus,
|
||||
} from '@/api/message'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { computed, reactive, ref, toRefs } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import List from './list.vue'
|
||||
@@ -42,6 +69,23 @@ const messageData = reactive<{
|
||||
messageList: [],
|
||||
})
|
||||
toRefs(messageData)
|
||||
const historyVisible = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const historyList = ref<NotificationDeliveryRecord[]>([])
|
||||
const historyPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
const historyColumns = [
|
||||
{ title: '渠道', dataIndex: 'channel_type', width: 90 },
|
||||
{ title: '目标', dataIndex: 'target', ellipsis: true, tooltip: true },
|
||||
{ title: '主题', dataIndex: 'subject', ellipsis: true, tooltip: true },
|
||||
{ title: '状态', slotName: 'status', width: 90 },
|
||||
{ title: '次数', dataIndex: 'attempt_count', width: 70 },
|
||||
{ title: '错误原因', dataIndex: 'last_error', ellipsis: true, tooltip: true },
|
||||
{ title: '操作', slotName: 'operation', width: 80 },
|
||||
]
|
||||
const tabList: TabItem[] = [
|
||||
{
|
||||
key: 'message',
|
||||
@@ -72,6 +116,59 @@ async function readMessage(data: MessageListType) {
|
||||
await setMessageStatus({ ids })
|
||||
fetchSourceData()
|
||||
}
|
||||
const fetchHistory = async () => {
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const res = await queryNotificationDeliveryHistory({
|
||||
page: historyPagination.current,
|
||||
page_size: historyPagination.pageSize,
|
||||
})
|
||||
const page = res.details || res.data
|
||||
historyList.value = page?.data || []
|
||||
historyPagination.total = page?.total || 0
|
||||
} catch (err) {
|
||||
Message.error('获取投递历史失败')
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
const onHistoryPageChange = (page: number) => {
|
||||
historyPagination.current = page
|
||||
fetchHistory()
|
||||
}
|
||||
const openHistory = () => {
|
||||
historyVisible.value = true
|
||||
}
|
||||
const canResendDelivery = (status: string) => {
|
||||
return status === 'failed' || status === 'retry_scheduled'
|
||||
}
|
||||
const resendDelivery = async (id: number) => {
|
||||
await resendNotificationDelivery(id)
|
||||
Message.success('补发请求已提交')
|
||||
fetchHistory()
|
||||
fetchSourceData()
|
||||
}
|
||||
const handleResend = (item: MessageRecord) => {
|
||||
resendDelivery(item.id)
|
||||
}
|
||||
const getDeliveryLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待发送',
|
||||
sent: '已发送',
|
||||
failed: '失败',
|
||||
retry_scheduled: '待重试',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
const getDeliveryColor = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'gray',
|
||||
sent: 'green',
|
||||
failed: 'red',
|
||||
retry_scheduled: 'orange',
|
||||
}
|
||||
return map[status] || 'gray'
|
||||
}
|
||||
const renderList = computed(() => {
|
||||
return messageData.messageList.filter((item) => messageType.value === item.type)
|
||||
})
|
||||
|
||||
@@ -9,10 +9,16 @@
|
||||
}"
|
||||
>
|
||||
<template #extra>
|
||||
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||
<a-space direction="vertical" :size="4" align="end">
|
||||
<a-tag v-if="item.deliveryStatus" :color="getDeliveryColor(item.deliveryStatus)">
|
||||
{{ getDeliveryLabel(item.deliveryStatus) }}
|
||||
</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||
<a-button v-if="canResend(item)" size="mini" type="text" @click.stop="onResend(item)">补发</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="item-wrap" @click="onItemClick(item)">
|
||||
<a-list-item-meta>
|
||||
@@ -53,7 +59,7 @@
|
||||
<a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link>
|
||||
</div>
|
||||
<div class="footer-wrap">
|
||||
<a-link>{{ $t('messageBox.viewMore') }}</a-link>
|
||||
<a-link @click="viewHistory">{{ $t('messageBox.viewMore') }}</a-link>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -75,7 +81,7 @@ const props = defineProps({
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['itemClick'])
|
||||
const emit = defineEmits(['itemClick', 'resend', 'viewHistory'])
|
||||
const allRead = () => {
|
||||
emit('itemClick', [...props.renderList])
|
||||
}
|
||||
@@ -85,6 +91,33 @@ const onItemClick = (item: MessageRecord) => {
|
||||
emit('itemClick', [item])
|
||||
}
|
||||
}
|
||||
const canResend = (item: MessageRecord) => {
|
||||
return item.deliveryStatus === 'failed' || item.deliveryStatus === 'retry_scheduled'
|
||||
}
|
||||
const onResend = (item: MessageRecord) => {
|
||||
emit('resend', item)
|
||||
}
|
||||
const viewHistory = () => {
|
||||
emit('viewHistory')
|
||||
}
|
||||
const getDeliveryLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待发送',
|
||||
sent: '已发送',
|
||||
failed: '失败',
|
||||
retry_scheduled: '待重试',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
const getDeliveryColor = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'gray',
|
||||
sent: 'green',
|
||||
failed: 'red',
|
||||
retry_scheduled: 'orange',
|
||||
}
|
||||
return map[status] || 'gray'
|
||||
}
|
||||
const showMax = 3
|
||||
</script>
|
||||
|
||||
|
||||
89
src/components/page-state/index.vue
Normal file
89
src/components/page-state/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="page-state" :class="[`page-state-${status}`]">
|
||||
<a-spin v-if="status === 'loading'" :tip="loadingText" class="page-state-spin">
|
||||
<slot />
|
||||
</a-spin>
|
||||
|
||||
<a-result v-else-if="status === 'error'" status="500" :title="errorTitle" :subtitle="errorText">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="$emit('retry')">重试</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
|
||||
<a-result v-else-if="status === 'unauthorized'" status="403" :title="unauthorizedTitle" :subtitle="unauthorizedText" />
|
||||
|
||||
<template v-else>
|
||||
<a-alert v-if="status === 'partial'" type="warning" show-icon class="page-state-alert">
|
||||
{{ partialText }}
|
||||
</a-alert>
|
||||
<slot />
|
||||
<a-empty v-if="status === 'empty'" :description="emptyText" class="page-state-empty" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
default: 'success',
|
||||
validator: (value: string) => ['loading', 'empty', 'error', 'success', 'partial', 'unauthorized'].includes(value),
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: '加载中',
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
errorTitle: {
|
||||
type: String,
|
||||
default: '加载失败',
|
||||
},
|
||||
errorText: {
|
||||
type: String,
|
||||
default: '数据暂时不可用,请稍后重试。',
|
||||
},
|
||||
partialText: {
|
||||
type: String,
|
||||
default: '部分数据加载失败,当前页面展示可用数据。',
|
||||
},
|
||||
unauthorizedTitle: {
|
||||
type: String,
|
||||
default: '无权限访问',
|
||||
},
|
||||
unauthorizedText: {
|
||||
type: String,
|
||||
default: '当前账号没有访问或操作该页面的权限。',
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'retry'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PageState',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page-state {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-state-spin {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-state-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.page-state-empty {
|
||||
padding: 32px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -20,43 +20,53 @@
|
||||
<a-divider style="margin-top: 0" />
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="bordered"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="scroll"
|
||||
:show-toolbar="showToolbar"
|
||||
:show-download="showDownload"
|
||||
:show-refresh="showRefresh"
|
||||
:show-density="showDensity"
|
||||
:show-column-setting="showColumnSetting"
|
||||
:download-button-text="downloadButtonText"
|
||||
:refresh-tooltip-text="refreshTooltipText"
|
||||
:density-tooltip-text="densityTooltipText"
|
||||
:column-setting-tooltip-text="columnSettingTooltipText"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
@density-change="handleDensityChange"
|
||||
@column-change="handleColumnChange"
|
||||
<PageState
|
||||
:status="stateStatus"
|
||||
:empty-text="emptyText"
|
||||
:error-text="errorText"
|
||||
:partial-text="partialText"
|
||||
:loading-text="loadingText"
|
||||
@retry="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<slot name="toolbar-left" />
|
||||
</template>
|
||||
<template #toolbar-right>
|
||||
<slot name="toolbar-right" />
|
||||
</template>
|
||||
<!-- 动态插槽透传 -->
|
||||
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
|
||||
<slot :name="col.slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</DataTable>
|
||||
<DataTable
|
||||
v-if="stateStatus !== 'empty' && stateStatus !== 'error' && stateStatus !== 'unauthorized'"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="bordered"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="responsiveScroll"
|
||||
:show-toolbar="showToolbar"
|
||||
:show-download="showDownload"
|
||||
:show-refresh="showRefresh"
|
||||
:show-density="showDensity"
|
||||
:show-column-setting="showColumnSetting"
|
||||
:download-button-text="downloadButtonText"
|
||||
:refresh-tooltip-text="refreshTooltipText"
|
||||
:density-tooltip-text="densityTooltipText"
|
||||
:column-setting-tooltip-text="columnSettingTooltipText"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
@density-change="handleDensityChange"
|
||||
@column-change="handleColumnChange"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<slot name="toolbar-left" />
|
||||
</template>
|
||||
<template #toolbar-right>
|
||||
<slot name="toolbar-right" />
|
||||
</template>
|
||||
<!-- 动态插槽透传 -->
|
||||
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
|
||||
<slot :name="col.slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</DataTable>
|
||||
</PageState>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -105,6 +115,26 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
status: {
|
||||
type: String as PropType<'loading' | 'empty' | 'error' | 'success' | 'partial' | 'unauthorized' | undefined>,
|
||||
default: undefined,
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: '加载中',
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
errorText: {
|
||||
type: String,
|
||||
default: '数据暂时不可用,请稍后重试。',
|
||||
},
|
||||
partialText: {
|
||||
type: String,
|
||||
default: '部分数据加载失败,当前页面展示可用数据。',
|
||||
},
|
||||
pagination: {
|
||||
type: Object as PropType<{
|
||||
current: number
|
||||
@@ -191,6 +221,17 @@ const slotColumns = computed(() => {
|
||||
return props.columns.filter((col) => col.slotName)
|
||||
})
|
||||
|
||||
const stateStatus = computed(() => {
|
||||
if (props.status) return props.status
|
||||
if (props.loading) return 'loading'
|
||||
if (!props.data.length) return 'empty'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const responsiveScroll = computed(() => {
|
||||
return props.scroll || { x: 'max-content' }
|
||||
})
|
||||
|
||||
const handleFormModelUpdate = (value: Record<string, any>) => {
|
||||
emit('update:formModel', value)
|
||||
}
|
||||
@@ -246,4 +287,10 @@ export default {
|
||||
.search-table-container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-table-container {
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user