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,442 @@
<template>
<div class="page-content">
<!-- 操作提示 -->
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
<template #title>
<div style="line-height: 1.8">
<p><strong>资产分配说明</strong></p>
<p>1. <strong>网卡批量分配</strong>仅分配选中的网卡资产</p>
<p>2. <strong>设备批量分配</strong>仅分配选中的设备资产</p>
<p>3. <strong>网卡+设备分配</strong>如果网卡有绑定设备将同时分配网卡和设备</p>
<p>4. 分配后资产所有权将转移至目标代理商</p>
</div>
</template>
</ElAlert>
<!-- 分配模式选择 -->
<ElCard shadow="never" style="margin-bottom: 20px">
<ElRadioGroup v-model="assignMode" size="large">
<ElRadioButton value="sim">网卡批量分配</ElRadioButton>
<ElRadioButton value="device">设备批量分配</ElRadioButton>
<ElRadioButton value="both">网卡+设备分配</ElRadioButton>
</ElRadioGroup>
</ElCard>
<!-- 网卡分配 -->
<ElCard v-if="assignMode === 'sim' || assignMode === 'both'" shadow="never" style="margin-bottom: 20px">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span style="font-weight: 500">选择网卡资产</span>
<ElButton type="primary" size="small" :disabled="selectedSims.length === 0" @click="showAssignDialog('sim')">
分配选中的 {{ selectedSims.length }} 张网卡
</ElButton>
</div>
</template>
<ElRow :gutter="12" style="margin-bottom: 16px">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="simSearchQuery" placeholder="ICCID/IMSI" clearable />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="simStatusFilter" placeholder="状态筛选" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="激活" value="active" />
<ElOption label="未激活" value="inactive" />
<ElOption label="停机" value="suspended" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElButton v-ripple @click="searchSims">搜索</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredSimData" index @selection-change="handleSimSelectionChange">
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn label="ICCID" prop="iccid" width="200" />
<ElTableColumn label="IMSI" prop="imsi" width="180" />
<ElTableColumn label="运营商" prop="operator" width="100">
<template #default="scope">
<ElTag size="small">{{ scope.row.operator }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag v-if="scope.row.status === 'active'" type="success" size="small">激活</ElTag>
<ElTag v-else-if="scope.row.status === 'inactive'" type="info" size="small">未激活</ElTag>
<ElTag v-else type="warning" size="small">停机</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="绑定设备" prop="deviceCode" width="150">
<template #default="scope">
<ElTag v-if="scope.row.deviceCode" type="primary" size="small">
{{ scope.row.deviceCode }}
</ElTag>
<span v-else style="color: var(--el-text-color-secondary)">未绑定</span>
</template>
</ElTableColumn>
<ElTableColumn label="剩余流量" prop="remainData" width="120" />
<ElTableColumn label="到期时间" prop="expireTime" width="180" />
</template>
</ArtTable>
</ElCard>
<!-- 设备分配 -->
<ElCard v-if="assignMode === 'device'" shadow="never">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span style="font-weight: 500">选择设备资产</span>
<ElButton type="primary" size="small" :disabled="selectedDevices.length === 0" @click="showAssignDialog('device')">
分配选中的 {{ selectedDevices.length }} 个设备
</ElButton>
</div>
</template>
<ElRow :gutter="12" style="margin-bottom: 16px">
<ElCol :xs="24" :sm="12" :lg="6">
<ElInput v-model="deviceSearchQuery" placeholder="设备编号/名称" clearable />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElSelect v-model="deviceTypeFilter" placeholder="设备类型" clearable style="width: 100%">
<ElOption label="全部" value="" />
<ElOption label="GPS定位器" value="gps" />
<ElOption label="智能水表" value="water_meter" />
<ElOption label="智能电表" value="electric_meter" />
</ElSelect>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="6">
<ElButton v-ripple @click="searchDevices">搜索</ElButton>
</ElCol>
</ElRow>
<ArtTable :data="filteredDeviceData" index @selection-change="handleDeviceSelectionChange">
<template #default>
<ElTableColumn type="selection" width="55" />
<ElTableColumn label="设备编号" prop="deviceCode" width="180" />
<ElTableColumn label="设备名称" prop="deviceName" min-width="180" />
<ElTableColumn label="设备类型" prop="deviceType" width="120">
<template #default="scope">
<ElTag size="small">{{ getDeviceTypeText(scope.row.deviceType) }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="绑定ICCID" prop="iccid" width="200">
<template #default="scope">
<ElTag v-if="scope.row.iccid" type="success" size="small">
{{ scope.row.iccid }}
</ElTag>
<span v-else style="color: var(--el-text-color-secondary)">未绑定</span>
</template>
</ElTableColumn>
<ElTableColumn label="在线状态" prop="onlineStatus" width="100">
<template #default="scope">
<ElTag v-if="scope.row.onlineStatus === 'online'" type="success" size="small">在线</ElTag>
<ElTag v-else type="info" size="small">离线</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="createTime" width="180" />
</template>
</ArtTable>
</ElCard>
<!-- 分配对话框 -->
<ElDialog v-model="assignDialogVisible" title="资产分配" width="600px" align-center>
<ElForm ref="formRef" :model="assignForm" :rules="assignRules" label-width="120px">
<ElFormItem label="分配类型">
<ElTag v-if="assignForm.type === 'sim'" type="primary">网卡资产</ElTag>
<ElTag v-else-if="assignForm.type === 'device'" type="success">设备资产</ElTag>
<ElTag v-else type="warning">网卡+设备</ElTag>
</ElFormItem>
<ElFormItem label="分配数量">
<div>
<span v-if="assignForm.type === 'sim'" style="font-size: 18px; font-weight: 600; color: var(--el-color-primary)">
{{ selectedSims.length }} 张网卡
</span>
<span v-else-if="assignForm.type === 'device'" style="font-size: 18px; font-weight: 600; color: var(--el-color-success)">
{{ selectedDevices.length }} 个设备
</span>
<span v-else style="font-size: 18px; font-weight: 600">
{{ selectedSims.length }} 张网卡 + {{ selectedSims.filter(s => s.deviceCode).length }} 个设备
</span>
</div>
</ElFormItem>
<ElFormItem label="目标代理商" prop="targetAgentId">
<ElSelect
v-model="assignForm.targetAgentId"
placeholder="请选择目标代理商"
filterable
style="width: 100%"
>
<ElOption
v-for="agent in agentList"
:key="agent.id"
:label="`${agent.agentName} (等级${agent.level})`"
:value="agent.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="分配说明" prop="remark">
<ElInput
v-model="assignForm.remark"
type="textarea"
:rows="3"
placeholder="请输入分配说明"
/>
</ElFormItem>
<ElAlert type="warning" :closable="false">
分配后资产所有权将转移至目标代理商原账号将无法管理这些资产
</ElAlert>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="assignDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleAssignSubmit">确认分配</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配记录 -->
<ElCard shadow="never" style="margin-top: 20px">
<template #header>
<span style="font-weight: 500">最近分配记录</span>
</template>
<ArtTable :data="assignHistory" index>
<template #default>
<ElTableColumn label="分配批次号" prop="batchNo" width="180" />
<ElTableColumn label="分配类型" prop="type" width="120">
<template #default="scope">
<ElTag v-if="scope.row.type === 'sim'" type="primary" size="small">网卡</ElTag>
<ElTag v-else-if="scope.row.type === 'device'" type="success" size="small">设备</ElTag>
<ElTag v-else type="warning" size="small">网卡+设备</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="分配数量" prop="quantity" width="100" />
<ElTableColumn label="目标代理商" prop="targetAgentName" min-width="150" />
<ElTableColumn label="分配说明" prop="remark" min-width="200" show-overflow-tooltip />
<ElTableColumn label="分配时间" prop="assignTime" width="180" />
<ElTableColumn label="操作人" prop="operator" width="100" />
</template>
</ArtTable>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({ name: 'AssetAssign' })
interface SimCard {
id: string
iccid: string
imsi: string
operator: string
status: string
deviceCode?: string
remainData: string
expireTime: string
}
interface Device {
id: string
deviceCode: string
deviceName: string
deviceType: string
iccid?: string
onlineStatus: string
createTime: string
}
const assignMode = ref('sim')
const simSearchQuery = ref('')
const simStatusFilter = ref('')
const deviceSearchQuery = ref('')
const deviceTypeFilter = ref('')
const assignDialogVisible = ref(false)
const formRef = ref<FormInstance>()
const selectedSims = ref<SimCard[]>([])
const selectedDevices = ref<Device[]>([])
const assignForm = reactive({
type: 'sim',
targetAgentId: '',
remark: ''
})
const assignRules = reactive<FormRules>({
targetAgentId: [{ required: true, message: '请选择目标代理商', trigger: 'change' }]
})
const agentList = ref([
{ id: '1', agentName: '华东区总代理', level: 1 },
{ id: '2', agentName: '华南区代理', level: 2 },
{ id: '3', agentName: '华北区代理', level: 1 }
])
const simMockData = ref<SimCard[]>([
{
id: '1',
iccid: '89860123456789012345',
imsi: '460012345678901',
operator: '中国移动',
status: 'active',
deviceCode: 'DEV001',
remainData: '50GB',
expireTime: '2026-12-31'
},
{
id: '2',
iccid: '89860123456789012346',
imsi: '460012345678902',
operator: '中国联通',
status: 'active',
remainData: '80GB',
expireTime: '2026-11-30'
}
])
const deviceMockData = ref<Device[]>([
{
id: '1',
deviceCode: 'DEV001',
deviceName: 'GPS定位器-001',
deviceType: 'gps',
iccid: '89860123456789012345',
onlineStatus: 'online',
createTime: '2026-01-01 10:00:00'
},
{
id: '2',
deviceCode: 'DEV002',
deviceName: '智能水表-002',
deviceType: 'water_meter',
iccid: '89860123456789012346',
onlineStatus: 'offline',
createTime: '2026-01-02 11:00:00'
}
])
const assignHistory = ref([
{
id: '1',
batchNo: 'ASSIGN202601090001',
type: 'sim',
quantity: 100,
targetAgentName: '华东区总代理',
remark: '批量分配给华东区',
assignTime: '2026-01-09 10:00:00',
operator: 'admin'
},
{
id: '2',
batchNo: 'ASSIGN202601080001',
type: 'both',
quantity: 50,
targetAgentName: '华南区代理',
remark: '网卡和设备一起分配',
assignTime: '2026-01-08 14:00:00',
operator: 'admin'
}
])
const filteredSimData = computed(() => {
let data = simMockData.value
if (simSearchQuery.value) {
data = data.filter(
(item) => item.iccid.includes(simSearchQuery.value) || item.imsi.includes(simSearchQuery.value)
)
}
if (simStatusFilter.value) {
data = data.filter((item) => item.status === simStatusFilter.value)
}
return data
})
const filteredDeviceData = computed(() => {
let data = deviceMockData.value
if (deviceSearchQuery.value) {
data = data.filter(
(item) => item.deviceCode.includes(deviceSearchQuery.value) || item.deviceName.includes(deviceSearchQuery.value)
)
}
if (deviceTypeFilter.value) {
data = data.filter((item) => item.deviceType === deviceTypeFilter.value)
}
return data
})
const getDeviceTypeText = (type: string) => {
const map: Record<string, string> = {
gps: 'GPS定位器',
water_meter: '智能水表',
electric_meter: '智能电表'
}
return map[type] || type
}
const searchSims = () => {}
const searchDevices = () => {}
const handleSimSelectionChange = (rows: SimCard[]) => {
selectedSims.value = rows
}
const handleDeviceSelectionChange = (rows: Device[]) => {
selectedDevices.value = rows
}
const showAssignDialog = (type: string) => {
assignForm.type = type
assignDialogVisible.value = true
}
const handleAssignSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) {
const agent = agentList.value.find((a) => a.id === assignForm.targetAgentId)
let quantity = 0
if (assignForm.type === 'sim') {
quantity = selectedSims.value.length
} else if (assignForm.type === 'device') {
quantity = selectedDevices.value.length
} else {
quantity = selectedSims.value.length
}
assignHistory.value.unshift({
id: Date.now().toString(),
batchNo: `ASSIGN${Date.now()}`,
type: assignForm.type,
quantity,
targetAgentName: agent?.agentName || '',
remark: assignForm.remark,
assignTime: new Date().toLocaleString('zh-CN'),
operator: 'admin'
})
assignDialogVisible.value = false
formRef.value.resetFields()
selectedSims.value = []
selectedDevices.value = []
ElMessage.success('资产分配成功')
}
})
}
</script>
<style lang="scss" scoped>
.page-content {
:deep(.el-radio-button__inner) {
padding: 12px 20px;
}
}
</style>