571 lines
19 KiB
Vue
571 lines
19 KiB
Vue
<template>
|
||
<div class="page-content">
|
||
<!-- 搜索和操作区 -->
|
||
<ElRow :gutter="12">
|
||
<ElCol :xs="24" :sm="12" :lg="6">
|
||
<ElInput v-model="searchQuery" placeholder="通知标题/内容" clearable />
|
||
</ElCol>
|
||
<ElCol :xs="24" :sm="12" :lg="6">
|
||
<ElSelect v-model="statusFilter" placeholder="状态筛选" clearable style="width: 100%">
|
||
<ElOption label="全部" value="" />
|
||
<ElOption label="待发送" value="pending" />
|
||
<ElOption label="发送中" value="sending" />
|
||
<ElOption label="已发送" value="sent" />
|
||
<ElOption label="发送失败" value="failed" />
|
||
</ElSelect>
|
||
</ElCol>
|
||
<ElCol :xs="24" :sm="12" :lg="6">
|
||
<ElSelect v-model="typeFilter" placeholder="通知类型" clearable style="width: 100%">
|
||
<ElOption label="全部" value="" />
|
||
<ElOption label="卡片更换" value="replace" />
|
||
<ElOption label="卡片激活" value="activate" />
|
||
<ElOption label="卡片停用" value="deactivate" />
|
||
<ElOption label="套餐变更" value="plan_change" />
|
||
</ElSelect>
|
||
</ElCol>
|
||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
|
||
<ElButton v-ripple type="primary" @click="showDialog('add')">新增通知</ElButton>
|
||
</ElCol>
|
||
</ElRow>
|
||
|
||
<!-- 通知列表 -->
|
||
<ArtTable :data="filteredData" index style="margin-top: 20px">
|
||
<template #default>
|
||
<ElTableColumn type="selection" width="55" />
|
||
<ElTableColumn label="通知标题" prop="title" min-width="200" show-overflow-tooltip />
|
||
<ElTableColumn label="通知类型" prop="type" width="120">
|
||
<template #default="scope">
|
||
<ElTag :type="getTypeTagType(scope.row.type)">
|
||
{{ getTypeText(scope.row.type) }}
|
||
</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="目标用户数" prop="targetCount" width="120">
|
||
<template #default="scope">
|
||
<span style="color: var(--el-color-primary)">{{ scope.row.targetCount }}</span>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="已发送" prop="sentCount" width="100">
|
||
<template #default="scope">
|
||
<span style="color: var(--el-color-success)">{{ scope.row.sentCount }}</span>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="失败数" prop="failCount" width="100">
|
||
<template #default="scope">
|
||
<span style="color: var(--el-color-danger)">{{ scope.row.failCount }}</span>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="发送进度" prop="progress" width="150">
|
||
<template #default="scope">
|
||
<ElProgress
|
||
:percentage="scope.row.progress"
|
||
:status="
|
||
scope.row.status === 'failed'
|
||
? 'exception'
|
||
: scope.row.status === 'sent'
|
||
? 'success'
|
||
: undefined
|
||
"
|
||
/>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="状态" prop="status" width="120">
|
||
<template #default="scope">
|
||
<ElTag v-if="scope.row.status === 'pending'" type="info">待发送</ElTag>
|
||
<ElTag v-else-if="scope.row.status === 'sending'" type="warning">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
发送中
|
||
</ElTag>
|
||
<ElTag v-else-if="scope.row.status === 'sent'" type="success">已发送</ElTag>
|
||
<ElTag v-else-if="scope.row.status === 'failed'" type="danger">发送失败</ElTag>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="发送方式" prop="sendMethod" width="120">
|
||
<template #default="scope">
|
||
<div style="display: flex; gap: 4px">
|
||
<ElTag v-for="method in scope.row.sendMethods" :key="method" size="small">
|
||
{{ getSendMethodText(method) }}
|
||
</ElTag>
|
||
</div>
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn label="创建时间" prop="createTime" width="180" />
|
||
<ElTableColumn label="发送时间" prop="sendTime" width="180">
|
||
<template #default="scope">
|
||
{{ scope.row.sendTime || '-' }}
|
||
</template>
|
||
</ElTableColumn>
|
||
<ElTableColumn fixed="right" label="操作" width="240">
|
||
<template #default="scope">
|
||
<el-button link :icon="View" @click="viewDetail(scope.row)">查看详情</el-button>
|
||
<el-button
|
||
v-if="scope.row.status === 'pending'"
|
||
link
|
||
type="primary"
|
||
:icon="Promotion"
|
||
@click="handleSend(scope.row)"
|
||
>
|
||
立即发送
|
||
</el-button>
|
||
<el-button
|
||
v-if="scope.row.status === 'pending'"
|
||
link
|
||
@click="showDialog('edit', scope.row)"
|
||
>
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
v-if="scope.row.status === 'pending' || scope.row.status === 'failed'"
|
||
link
|
||
type="danger"
|
||
@click="handleDelete(scope.row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</ElTableColumn>
|
||
</template>
|
||
</ArtTable>
|
||
|
||
<!-- 新增/编辑通知对话框 -->
|
||
<ElDialog
|
||
v-model="dialogVisible"
|
||
:title="dialogType === 'add' ? '新增换卡通知' : '编辑换卡通知'"
|
||
width="700px"
|
||
align-center
|
||
>
|
||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||
<ElFormItem label="通知标题" prop="title">
|
||
<ElInput
|
||
v-model="form.title"
|
||
placeholder="请输入通知标题"
|
||
maxlength="50"
|
||
show-word-limit
|
||
/>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem label="通知类型" prop="type">
|
||
<ElRadioGroup v-model="form.type">
|
||
<ElRadio value="replace">卡片更换</ElRadio>
|
||
<ElRadio value="activate">卡片激活</ElRadio>
|
||
<ElRadio value="deactivate">卡片停用</ElRadio>
|
||
<ElRadio value="plan_change">套餐变更</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem label="通知内容" prop="content">
|
||
<ElInput
|
||
v-model="form.content"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入通知内容"
|
||
maxlength="500"
|
||
show-word-limit
|
||
/>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem label="目标用户" prop="targetType">
|
||
<ElRadioGroup v-model="form.targetType">
|
||
<ElRadio value="all">全部用户</ElRadio>
|
||
<ElRadio value="specific">指定用户</ElRadio>
|
||
<ElRadio value="batch">批量导入</ElRadio>
|
||
</ElRadioGroup>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem v-if="form.targetType === 'specific'" label="用户列表">
|
||
<ElSelect
|
||
v-model="form.targetUsers"
|
||
multiple
|
||
placeholder="请选择目标用户"
|
||
style="width: 100%"
|
||
>
|
||
<ElOption label="张三 (13800138000)" value="user1" />
|
||
<ElOption label="李四 (13900139000)" value="user2" />
|
||
<ElOption label="王五 (13700137000)" value="user3" />
|
||
</ElSelect>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem v-if="form.targetType === 'batch'" label="用户文件">
|
||
<ElUpload
|
||
:action="uploadUrl"
|
||
:limit="1"
|
||
:on-exceed="handleExceed"
|
||
accept=".xlsx,.xls,.txt"
|
||
>
|
||
<ElButton type="primary">选择文件</ElButton>
|
||
<template #tip>
|
||
<div class="el-upload__tip">支持 Excel 或 TXT 格式,每行一个用户手机号</div>
|
||
</template>
|
||
</ElUpload>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem label="发送方式" prop="sendMethods">
|
||
<ElCheckboxGroup v-model="form.sendMethods">
|
||
<ElCheckbox value="sms">短信</ElCheckbox>
|
||
<ElCheckbox value="email">邮件</ElCheckbox>
|
||
<ElCheckbox value="app">App推送</ElCheckbox>
|
||
</ElCheckboxGroup>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem label="定时发送">
|
||
<ElSwitch v-model="form.scheduleSend" />
|
||
<span style="margin-left: 8px; color: var(--el-text-color-secondary)">
|
||
{{ form.scheduleSend ? '启用定时发送' : '立即发送' }}
|
||
</span>
|
||
</ElFormItem>
|
||
|
||
<ElFormItem v-if="form.scheduleSend" label="发送时间" prop="scheduleTime">
|
||
<ElDatePicker
|
||
v-model="form.scheduleTime"
|
||
type="datetime"
|
||
placeholder="选择发送时间"
|
||
style="width: 100%"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleSubmit(formRef)">
|
||
{{ form.scheduleSend ? '创建定时任务' : '保存' }}
|
||
</ElButton>
|
||
</div>
|
||
</template>
|
||
</ElDialog>
|
||
|
||
<!-- 详情对话框 -->
|
||
<ElDialog v-model="detailDialogVisible" title="通知详情" width="800px" align-center>
|
||
<ElDescriptions :column="2" border>
|
||
<ElDescriptionsItem label="通知标题">{{ currentDetail.title }}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="通知类型">
|
||
<ElTag :type="getTypeTagType(currentDetail.type)">
|
||
{{ getTypeText(currentDetail.type) }}
|
||
</ElTag>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="目标用户数">{{ currentDetail.targetCount }}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="已发送数">
|
||
<span style="color: var(--el-color-success)">{{ currentDetail.sentCount }}</span>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="失败数">
|
||
<span style="color: var(--el-color-danger)">{{ currentDetail.failCount }}</span>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="发送状态">
|
||
<ElTag v-if="currentDetail.status === 'sent'" type="success">已发送</ElTag>
|
||
<ElTag v-else-if="currentDetail.status === 'sending'" type="warning">发送中</ElTag>
|
||
<ElTag v-else-if="currentDetail.status === 'failed'" type="danger">发送失败</ElTag>
|
||
<ElTag v-else type="info">待发送</ElTag>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="发送方式" :span="2">
|
||
<ElTag
|
||
v-for="method in currentDetail.sendMethods"
|
||
:key="method"
|
||
size="small"
|
||
style="margin-right: 4px"
|
||
>
|
||
{{ getSendMethodText(method) }}
|
||
</ElTag>
|
||
</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="创建时间">{{ currentDetail.createTime }}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="发送时间">{{
|
||
currentDetail.sendTime || '-'
|
||
}}</ElDescriptionsItem>
|
||
<ElDescriptionsItem label="通知内容" :span="2">
|
||
{{ currentDetail.content }}
|
||
</ElDescriptionsItem>
|
||
</ElDescriptions>
|
||
|
||
<template #footer>
|
||
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { View, Loading, Promotion } from '@element-plus/icons-vue'
|
||
import type { FormInstance, FormRules, UploadProps } from 'element-plus'
|
||
|
||
defineOptions({ name: 'CardChangeNotice' })
|
||
|
||
interface Notice {
|
||
id: string
|
||
title: string
|
||
type: 'replace' | 'activate' | 'deactivate' | 'plan_change'
|
||
content: string
|
||
targetCount: number
|
||
sentCount: number
|
||
failCount: number
|
||
progress: number
|
||
status: 'pending' | 'sending' | 'sent' | 'failed'
|
||
sendMethods: string[]
|
||
createTime: string
|
||
sendTime?: string
|
||
}
|
||
|
||
const searchQuery = ref('')
|
||
const statusFilter = ref('')
|
||
const typeFilter = ref('')
|
||
const dialogVisible = ref(false)
|
||
const detailDialogVisible = ref(false)
|
||
const dialogType = ref<'add' | 'edit'>('add')
|
||
const formRef = ref<FormInstance>()
|
||
const uploadUrl = ref('/api/batch/upload-users')
|
||
|
||
const form = reactive({
|
||
title: '',
|
||
type: 'replace',
|
||
content: '',
|
||
targetType: 'all',
|
||
targetUsers: [] as string[],
|
||
sendMethods: ['sms'],
|
||
scheduleSend: false,
|
||
scheduleTime: null as Date | null
|
||
})
|
||
|
||
const rules = reactive<FormRules>({
|
||
title: [{ required: true, message: '请输入通知标题', trigger: 'blur' }],
|
||
type: [{ required: true, message: '请选择通知类型', trigger: 'change' }],
|
||
content: [{ required: true, message: '请输入通知内容', trigger: 'blur' }],
|
||
targetType: [{ required: true, message: '请选择目标用户类型', trigger: 'change' }],
|
||
sendMethods: [
|
||
{
|
||
type: 'array',
|
||
required: true,
|
||
message: '请至少选择一种发送方式',
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
})
|
||
|
||
const mockData = ref<Notice[]>([
|
||
{
|
||
id: '1',
|
||
title: 'SIM卡更换通知-2026年1月批次',
|
||
type: 'replace',
|
||
content: '尊敬的用户,您的物联网卡将于近期进行更换,新卡将在3个工作日内寄出,请注意查收。',
|
||
targetCount: 1000,
|
||
sentCount: 980,
|
||
failCount: 20,
|
||
progress: 100,
|
||
status: 'sent',
|
||
sendMethods: ['sms', 'app'],
|
||
createTime: '2026-01-09 09:00:00',
|
||
sendTime: '2026-01-09 10:00:00'
|
||
},
|
||
{
|
||
id: '2',
|
||
title: '卡片激活成功通知',
|
||
type: 'activate',
|
||
content: '恭喜!您的物联网卡已成功激活,现在可以正常使用了。',
|
||
targetCount: 500,
|
||
sentCount: 350,
|
||
failCount: 5,
|
||
progress: 71,
|
||
status: 'sending',
|
||
sendMethods: ['sms', 'email'],
|
||
createTime: '2026-01-09 11:30:00'
|
||
},
|
||
{
|
||
id: '3',
|
||
title: '套餐变更提醒',
|
||
type: 'plan_change',
|
||
content: '您好,您的套餐将于2026年2月1日起变更为新套餐,详情请登录系统查看。',
|
||
targetCount: 800,
|
||
sentCount: 0,
|
||
failCount: 0,
|
||
progress: 0,
|
||
status: 'pending',
|
||
sendMethods: ['sms'],
|
||
createTime: '2026-01-09 14:00:00'
|
||
}
|
||
])
|
||
|
||
const currentDetail = ref<Notice>({
|
||
id: '',
|
||
title: '',
|
||
type: 'replace',
|
||
content: '',
|
||
targetCount: 0,
|
||
sentCount: 0,
|
||
failCount: 0,
|
||
progress: 0,
|
||
status: 'pending',
|
||
sendMethods: [],
|
||
createTime: ''
|
||
})
|
||
|
||
const filteredData = computed(() => {
|
||
let data = mockData.value
|
||
|
||
if (searchQuery.value) {
|
||
data = data.filter(
|
||
(item) => item.title.includes(searchQuery.value) || item.content.includes(searchQuery.value)
|
||
)
|
||
}
|
||
|
||
if (statusFilter.value) {
|
||
data = data.filter((item) => item.status === statusFilter.value)
|
||
}
|
||
|
||
if (typeFilter.value) {
|
||
data = data.filter((item) => item.type === typeFilter.value)
|
||
}
|
||
|
||
return data
|
||
})
|
||
|
||
const getTypeText = (type: string) => {
|
||
const map: Record<string, string> = {
|
||
replace: '卡片更换',
|
||
activate: '卡片激活',
|
||
deactivate: '卡片停用',
|
||
plan_change: '套餐变更'
|
||
}
|
||
return map[type] || '未知'
|
||
}
|
||
|
||
const getTypeTagType = (type: string) => {
|
||
const map: Record<string, any> = {
|
||
replace: 'warning',
|
||
activate: 'success',
|
||
deactivate: 'danger',
|
||
plan_change: 'primary'
|
||
}
|
||
return map[type] || ''
|
||
}
|
||
|
||
const getSendMethodText = (method: string) => {
|
||
const map: Record<string, string> = {
|
||
sms: '短信',
|
||
email: '邮件',
|
||
app: 'App'
|
||
}
|
||
return map[method] || method
|
||
}
|
||
|
||
const handleSearch = () => {}
|
||
|
||
const showDialog = (type: 'add' | 'edit', row?: Notice) => {
|
||
dialogType.value = type
|
||
dialogVisible.value = true
|
||
if (type === 'edit' && row) {
|
||
Object.assign(form, {
|
||
title: row.title,
|
||
type: row.type,
|
||
content: row.content,
|
||
targetType: 'all',
|
||
sendMethods: row.sendMethods,
|
||
scheduleSend: false,
|
||
scheduleTime: null
|
||
})
|
||
} else {
|
||
Object.assign(form, {
|
||
title: '',
|
||
type: 'replace',
|
||
content: '',
|
||
targetType: 'all',
|
||
targetUsers: [],
|
||
sendMethods: ['sms'],
|
||
scheduleSend: false,
|
||
scheduleTime: null
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||
if (!formEl) return
|
||
await formEl.validate((valid) => {
|
||
if (valid) {
|
||
if (dialogType.value === 'add') {
|
||
mockData.value.unshift({
|
||
id: Date.now().toString(),
|
||
title: form.title,
|
||
type: form.type as any,
|
||
content: form.content,
|
||
targetCount: form.targetType === 'all' ? 1000 : form.targetUsers.length,
|
||
sentCount: 0,
|
||
failCount: 0,
|
||
progress: 0,
|
||
status: 'pending',
|
||
sendMethods: form.sendMethods,
|
||
createTime: new Date().toLocaleString('zh-CN')
|
||
})
|
||
ElMessage.success('通知创建成功')
|
||
} else {
|
||
ElMessage.success('通知更新成功')
|
||
}
|
||
dialogVisible.value = false
|
||
formEl.resetFields()
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleSend = (row: Notice) => {
|
||
ElMessageBox.confirm(`确定立即发送通知"${row.title}"吗?`, '发送确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
row.status = 'sending'
|
||
ElMessage.success('通知发送中...')
|
||
|
||
// 模拟发送进度
|
||
const timer = setInterval(() => {
|
||
if (row.progress < 100) {
|
||
row.progress += 10
|
||
row.sentCount = Math.floor((row.targetCount * row.progress) / 100)
|
||
} else {
|
||
clearInterval(timer)
|
||
row.status = 'sent'
|
||
row.sendTime = new Date().toLocaleString('zh-CN')
|
||
ElMessage.success('通知发送完成')
|
||
}
|
||
}, 500)
|
||
})
|
||
}
|
||
|
||
const handleDelete = (row: Notice) => {
|
||
ElMessageBox.confirm('确定删除该通知吗?', '删除确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'error'
|
||
}).then(() => {
|
||
const index = mockData.value.findIndex((item) => item.id === row.id)
|
||
if (index !== -1) mockData.value.splice(index, 1)
|
||
ElMessage.success('删除成功')
|
||
})
|
||
}
|
||
|
||
const viewDetail = (row: Notice) => {
|
||
currentDetail.value = { ...row }
|
||
detailDialogVisible.value = true
|
||
}
|
||
|
||
const handleExceed: UploadProps['onExceed'] = () => {
|
||
ElMessage.warning('最多只能上传1个文件')
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-content {
|
||
:deep(.is-loading) {
|
||
margin-right: 4px;
|
||
animation: rotating 2s linear infinite;
|
||
}
|
||
|
||
@keyframes rotating {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
}
|
||
</style>
|