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,523 @@
<template>
<div class="page-content">
<!-- 搜索和筛选区 -->
<ElRow :gutter="12">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="searchQuery" placeholder="申请单号/ICCID" 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="processing" />
<ElOption label="已完成" value="completed" />
<ElOption label="已拒绝" value="rejected" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElDatePicker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 100%"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton v-ripple @click="handleSearch">搜索</ElButton>
<ElButton v-ripple @click="exportData">导出</ElButton>
</ElCol>
</ElRow>
<!-- 统计卡片 -->
<ElRow :gutter="20" style="margin: 20px 0">
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">待处理</div>
<div class="stat-value" style="color: var(--el-color-warning)">{{ statistics.pending }}</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-warning)"><Clock /></el-icon>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">处理中</div>
<div class="stat-value" style="color: var(--el-color-primary)">{{ statistics.processing }}</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-primary)"><Loading /></el-icon>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">已完成</div>
<div class="stat-value" style="color: var(--el-color-success)">{{ statistics.completed }}</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-success)"><CircleCheck /></el-icon>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElCard shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-label">已拒绝</div>
<div class="stat-value" style="color: var(--el-color-danger)">{{ statistics.rejected }}</div>
</div>
<el-icon class="stat-icon" style="color: var(--el-color-danger)"><CircleClose /></el-icon>
</ElCard>
</ElCol>
</ElRow>
<!-- 换卡申请列表 -->
<ArtTable :data="filteredData" index>
<template #default>
<ElTableColumn label="申请单号" prop="requestNo" width="180" />
<ElTableColumn label="旧卡ICCID" prop="oldIccid" width="200" />
<ElTableColumn label="申请人" prop="applicant" width="120" />
<ElTableColumn label="联系电话" prop="phone" width="130" />
<ElTableColumn label="换卡原因" prop="reason" min-width="180" show-overflow-tooltip />
<ElTableColumn label="新卡ICCID" prop="newIccid" width="200">
<template #default="scope">
<ElTag v-if="scope.row.newIccid" type="success" size="small">
{{ scope.row.newIccid }}
</ElTag>
<span v-else style="color: var(--el-text-color-secondary)">待填充</span>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'pending'" type="warning">待处理</ElTag>
<ElTag v-else-if="scope.row.status === 'processing'" type="primary">处理中</ElTag>
<ElTag v-else-if="scope.row.status === 'completed'" type="success">已完成</ElTag>
<ElTag v-else type="danger">已拒绝</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="申请时间" prop="applyTime" width="180" />
<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"
@click="handleProcess(scope.row)"
>
处理
</el-button>
<el-button
v-if="scope.row.status === 'processing'"
link
type="success"
@click="fillNewIccid(scope.row)"
>
填充新卡
</el-button>
<el-button
v-if="scope.row.status === 'pending'"
link
type="danger"
@click="handleReject(scope.row)"
>
拒绝
</el-button>
</template>
</ElTableColumn>
</template>
</ArtTable>
<!-- 详情对话框 -->
<ElDialog v-model="detailDialogVisible" title="换卡申请详情" width="800px" align-center>
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="申请单号">{{ currentRequest.requestNo }}</ElDescriptionsItem>
<ElDescriptionsItem label="申请人">{{ currentRequest.applicant }}</ElDescriptionsItem>
<ElDescriptionsItem label="联系电话">{{ currentRequest.phone }}</ElDescriptionsItem>
<ElDescriptionsItem label="申请时间">{{ currentRequest.applyTime }}</ElDescriptionsItem>
<ElDescriptionsItem label="旧卡ICCID" :span="2">
<ElTag type="warning">{{ currentRequest.oldIccid }}</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="新卡ICCID" :span="2">
<ElTag v-if="currentRequest.newIccid" type="success">{{ currentRequest.newIccid }}</ElTag>
<span v-else style="color: var(--el-text-color-secondary)">待填充</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="换卡原因" :span="2">
{{ currentRequest.reason }}
</ElDescriptionsItem>
<ElDescriptionsItem label="详细说明" :span="2">
{{ currentRequest.description || '无' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag v-if="currentRequest.status === 'pending'" type="warning">待处理</ElTag>
<ElTag v-else-if="currentRequest.status === 'processing'" type="primary">处理中</ElTag>
<ElTag v-else-if="currentRequest.status === 'completed'" type="success">已完成</ElTag>
<ElTag v-else type="danger">已拒绝</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="处理人">
{{ currentRequest.processor || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="处理时间" :span="2">
{{ currentRequest.processTime || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentRequest.rejectReason" label="拒绝原因" :span="2">
<span style="color: var(--el-color-danger)">{{ currentRequest.rejectReason }}</span>
</ElDescriptionsItem>
</ElDescriptions>
<template #footer>
<ElButton @click="detailDialogVisible = false">关闭</ElButton>
</template>
</ElDialog>
<!-- 填充新卡对话框 -->
<ElDialog v-model="fillDialogVisible" title="填充新卡ICCID" width="500px" align-center>
<ElForm ref="fillFormRef" :model="fillForm" :rules="fillRules" label-width="100px">
<ElFormItem label="旧卡ICCID">
<ElInput :value="currentRequest.oldIccid" disabled />
</ElFormItem>
<ElFormItem label="新卡ICCID" prop="newIccid">
<ElInput v-model="fillForm.newIccid" placeholder="请输入新卡ICCID" maxlength="20" />
</ElFormItem>
<ElFormItem label="验证新卡">
<ElButton @click="validateNewIccid">验证ICCID</ElButton>
<div v-if="validationResult" style="margin-top: 8px">
<ElTag v-if="validationResult === 'success'" type="success" size="small">
验证通过该卡可用
</ElTag>
<ElTag v-else type="danger" size="small">
验证失败{{ validationMessage }}
</ElTag>
</div>
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="fillForm.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</ElFormItem>
<ElAlert type="info" :closable="false">
填充新卡后系统将自动完成换卡操作旧卡将被停用
</ElAlert>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="fillDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleFillSubmit">确认填充</ElButton>
</div>
</template>
</ElDialog>
<!-- 拒绝对话框 -->
<ElDialog v-model="rejectDialogVisible" title="拒绝换卡申请" width="500px" align-center>
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="100px">
<ElFormItem label="拒绝原因" prop="reason">
<ElInput
v-model="rejectForm.reason"
type="textarea"
:rows="4"
placeholder="请输入拒绝原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
<ElButton type="danger" @click="handleRejectSubmit">确认拒绝</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { View, Clock, Loading as LoadingIcon, CircleCheck, CircleClose } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'CardReplacementRequest' })
interface ReplacementRequest {
id: string
requestNo: string
oldIccid: string
newIccid?: string
applicant: string
phone: string
reason: string
description?: string
status: 'pending' | 'processing' | 'completed' | 'rejected'
applyTime: string
processor?: string
processTime?: string
rejectReason?: string
}
const searchQuery = ref('')
const statusFilter = ref('')
const dateRange = ref<[Date, Date] | null>(null)
const detailDialogVisible = ref(false)
const fillDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const fillFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const validationResult = ref<string>('')
const validationMessage = ref('')
const statistics = reactive({
pending: 15,
processing: 8,
completed: 102,
rejected: 5
})
const fillForm = reactive({
newIccid: '',
remark: ''
})
const fillRules = reactive<FormRules>({
newIccid: [
{ required: true, message: '请输入新卡ICCID', trigger: 'blur' },
{ len: 20, message: 'ICCID长度必须为20位', trigger: 'blur' }
]
})
const rejectForm = reactive({
reason: ''
})
const rejectRules = reactive<FormRules>({
reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const mockData = ref<ReplacementRequest[]>([
{
id: '1',
requestNo: 'REP202601090001',
oldIccid: '89860123456789012345',
applicant: '张三',
phone: '13800138000',
reason: '卡片损坏',
description: '卡片物理损坏无法使用',
status: 'pending',
applyTime: '2026-01-09 09:30:00'
},
{
id: '2',
requestNo: 'REP202601080002',
oldIccid: '89860123456789012346',
newIccid: '89860123456789012350',
applicant: '李四',
phone: '13900139000',
reason: '信号不稳定',
description: '长期信号不稳定,影响使用',
status: 'processing',
applyTime: '2026-01-08 14:20:00',
processor: 'admin'
},
{
id: '3',
requestNo: 'REP202601070003',
oldIccid: '89860123456789012347',
newIccid: '89860123456789012351',
applicant: '王五',
phone: '13700137000',
reason: '卡片丢失',
description: '卡片意外丢失',
status: 'completed',
applyTime: '2026-01-07 10:00:00',
processor: 'admin',
processTime: '2026-01-07 15:30:00'
},
{
id: '4',
requestNo: 'REP202601060004',
oldIccid: '89860123456789012348',
applicant: '赵六',
phone: '13600136000',
reason: '套餐到期',
description: '套餐到期需要换新卡',
status: 'rejected',
applyTime: '2026-01-06 11:00:00',
processor: 'admin',
processTime: '2026-01-06 12:00:00',
rejectReason: '套餐到期应该续费而不是换卡'
}
])
const currentRequest = ref<ReplacementRequest>({
id: '',
requestNo: '',
oldIccid: '',
applicant: '',
phone: '',
reason: '',
status: 'pending',
applyTime: ''
})
const filteredData = computed(() => {
let data = mockData.value
if (searchQuery.value) {
data = data.filter(
(item) =>
item.requestNo.includes(searchQuery.value) ||
item.oldIccid.includes(searchQuery.value) ||
(item.newIccid && item.newIccid.includes(searchQuery.value))
)
}
if (statusFilter.value) {
data = data.filter((item) => item.status === statusFilter.value)
}
return data
})
const handleSearch = () => {}
const exportData = () => {
ElMessage.success('数据导出中...')
}
const viewDetail = (row: ReplacementRequest) => {
currentRequest.value = { ...row }
detailDialogVisible.value = true
}
const handleProcess = (row: ReplacementRequest) => {
ElMessageBox.confirm('确定要处理该换卡申请吗?', '处理确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}).then(() => {
row.status = 'processing'
row.processor = 'admin'
ElMessage.success('已标记为处理中')
})
}
const fillNewIccid = (row: ReplacementRequest) => {
currentRequest.value = { ...row }
fillForm.newIccid = ''
fillForm.remark = ''
validationResult.value = ''
fillDialogVisible.value = true
}
const validateNewIccid = () => {
if (!fillForm.newIccid) {
ElMessage.warning('请先输入新卡ICCID')
return
}
if (fillForm.newIccid.length !== 20) {
validationResult.value = 'error'
validationMessage.value = 'ICCID长度必须为20位'
return
}
// 模拟验证
setTimeout(() => {
const exists = mockData.value.some((item) => item.oldIccid === fillForm.newIccid)
if (exists) {
validationResult.value = 'error'
validationMessage.value = '该ICCID已被使用'
} else {
validationResult.value = 'success'
validationMessage.value = ''
ElMessage.success('验证通过')
}
}, 500)
}
const handleFillSubmit = async () => {
if (!fillFormRef.value) return
if (validationResult.value !== 'success') {
ElMessage.warning('请先验证新卡ICCID')
return
}
await fillFormRef.value.validate((valid) => {
if (valid) {
const index = mockData.value.findIndex((item) => item.id === currentRequest.value.id)
if (index !== -1) {
mockData.value[index].newIccid = fillForm.newIccid
mockData.value[index].status = 'completed'
mockData.value[index].processTime = new Date().toLocaleString('zh-CN')
statistics.processing--
statistics.completed++
}
fillDialogVisible.value = false
ElMessage.success('新卡填充成功,换卡操作已完成')
}
})
}
const handleReject = (row: ReplacementRequest) => {
currentRequest.value = { ...row }
rejectForm.reason = ''
rejectDialogVisible.value = true
}
const handleRejectSubmit = async () => {
if (!rejectFormRef.value) return
await rejectFormRef.value.validate((valid) => {
if (valid) {
const index = mockData.value.findIndex((item) => item.id === currentRequest.value.id)
if (index !== -1) {
mockData.value[index].status = 'rejected'
mockData.value[index].rejectReason = rejectForm.reason
mockData.value[index].processor = 'admin'
mockData.value[index].processTime = new Date().toLocaleString('zh-CN')
statistics.pending--
statistics.rejected++
}
rejectDialogVisible.value = false
ElMessage.success('已拒绝该换卡申请')
}
})
}
</script>
<style lang="scss" scoped>
.page-content {
.stat-card {
:deep(.el-card__body) {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
}
.stat-content {
.stat-label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.stat-icon {
font-size: 40px;
opacity: 0.6;
}
}
}
</style>