Files
one-pipe-system/src/views/batch/card-change-notice/index.vue
2026-01-23 17:18:24 +08:00

571 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>