Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

View File

@@ -0,0 +1,546 @@
<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) {
animation: rotating 2s linear infinite;
margin-right: 4px;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
</style>